Skip to content

Minimal python package support#1881

Open
abstractedfox wants to merge 41 commits into
cloudflare:mainfrom
abstractedfox:python-packages
Open

Minimal python package support#1881
abstractedfox wants to merge 41 commits into
cloudflare:mainfrom
abstractedfox:python-packages

Conversation

@abstractedfox

@abstractedfox abstractedfox commented Jul 6, 2026

Copy link
Copy Markdown
Member

This implements the beginning of package support for dynamic python workers. At present, it allows defining a flat list of pure Python packages as dependencies, which it installs in the dynamic worker without dependency resolution

This is the beginning of work on this feature, it's a little comment-heavy
to that end and has some patterns that may not be permanent (these have
generally been noted as such). As of this commit, there are tests that
start to poke at this, namely the one that brings in fastapi, which currently
fails due to an extension it doesn't like.
Proof of concept for package support in dynamic Python workers.
As of this commit, it can retrieve an sdist with a flat layout from pypi and
install it into the virtual filesystem. It doesn't do dependency resolution,
so the current tests fail when attempting to import FastAPI's deps.
Used sdists before, now it uses wheels. Pre cleanup commit
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ae50567

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

This test only needs to confirm that retrieval of a hard list of pure python packages, without
dependency resolution, works.
This seems to be specific to JS packages
This dependendency caused issues when working in a container, but its removal
was purely for my sake

This reverts commit 4f8990d.
Also makes PyprojectToml interface less stringent
These got caught by the checks done by `pnpm run check`
@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1881

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1881

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1881

create-think

npm i https://pkg.pr.new/create-think@1881

hono-agents

npm i https://pkg.pr.new/hono-agents@1881

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1881

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1881

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1881

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1881

commit: ae50567

@abstractedfox abstractedfox changed the title Draft: Python package progress Minimal python package support Jul 8, 2026
@abstractedfox
abstractedfox marked this pull request as ready for review July 8, 2026 18:32

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 9 potential issues.

Open in Devin Review

Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/tests/e2e.test.ts
Comment thread packages/worker-bundler/src/tests/wrangler.jsonc Outdated
Comment thread packages/worker-bundler/src/index.ts
Comment thread packages/worker-bundler/src/installer.ts
Comment on lines +148 to +155
for (const path of fileSystem.list()) {
if (path !== "pyproject.toml") {
const content = fileSystem.read(path);
if (content !== null) {
modules[path] = content;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Python modules output includes all filesystem files, potentially including node_modules

The Python worker path at packages/worker-bundler/src/index.ts:148-155 iterates over all files in the filesystem (except pyproject.toml) and includes them in the output modules. If a project has both package.json with npm dependencies and a Python entry point, the installed node_modules/ tree would be included in the modules output. This could result in a very large modules object being passed to the Worker Loader. Consider filtering out node_modules/ and python_modules/ metadata directories.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just files specified by the user in the createWorker call, right? So this AI review comment seems wrong.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At that point it should be those files + all the packages that were just added, so my assumption is anything that's in there would've been put there deliberately

Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/installer.ts

@dom96 dom96 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall approach looks good! Left a few comments with suggestions.

CC @hoodmane @ryanking13 @threepointone for reviews too

Comment thread packages/worker-bundler/src/index.ts Outdated
Comment on lines +148 to +155
for (const path of fileSystem.list()) {
if (path !== "pyproject.toml") {
const content = fileSystem.read(path);
if (content !== null) {
modules[path] = content;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are just files specified by the user in the createWorker call, right? So this AI review comment seems wrong.

Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/installer.ts Outdated
Comment thread packages/worker-bundler/src/installer.ts Outdated
Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/tests/e2e.test.ts Outdated
Comment thread packages/worker-bundler/src/tests/e2e.test.ts

@ryanking13 ryanking13 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments, but it looks like a good starting point.

Not all of my reviews need to be addressed in this PR. Some of my comments are for FYI, as you'll eventually handle them. If you consider some of my comments as out-of-scope of this PR and would want to hand it in the follow up PRs (which is a good practice), then please add a comment about it.

Comment thread packages/worker-bundler/src/tests/wrangler.jsonc Outdated
Comment thread packages/worker-bundler/src/index.ts Outdated
mainModule: entryPoint,
modules,
wranglerConfig: {
compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this default compat date selected? How does the JS worker handle this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recall just choosing it as an arbitrary date that I thought would work, I can put a newer one. The JS worker as in the worker that creates the dynamic worker?

@ryanking13 ryanking13 Jul 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, following whatever the JS worker is doing would be a good idea.

Comment thread packages/worker-bundler/src/index.ts Outdated
modules,
wranglerConfig: {
compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",
compatibilityFlags: wranglerConfig?.compatibilityFlags ?? [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will always need to add python_workers flag to make the worker work. So maybe we need to inject one if this flag does not exist?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in to the user-defined compatibility flags, in the event the user passed flags that didn't include python_workers? I can add that

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that should no longer be needed for Python dynamic workers. You can run python dynamic workers without that flag.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dom96 Should I remove it?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

*/

import * as semver from "semver";
import { unzipSync } from "fflate";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have CompressionStream support in CF worker. fflate claims that the performance of CompressionSTream is bad in the browser, but that might not be true.

you don't need to use CompressionStream in this PR as it is an implementation detail that we can change anytime and the performance is not our concern at this point, but it would be nice to add a comment in the code why you decided to use this package and things that need to be checked (performance compared to the CompressionStream) later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually only did it this way because I dryly assumed I'd need a lib for it, I'd much rather save a dependency so I'm glad you told me that

Comment thread packages/worker-bundler/src/installer.ts Outdated
const response = await fetchWithTimeout(
wheelUrl,
{},
DEFAULT_TIMEOUT_MS * 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why not double the DEFAULT_TIMEOUT_MS itself?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because it's not clear to me whether that would have undesirable effects elsewhere, there are other places where this function is called with the default duration. I ended up just staying with what the JS one was doing

Comment on lines +417 to +420
// Skip wheel metadata and data directories
if (path.includes(".dist-info/") || path.includes(".data/")) {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? These are important files used in runtime as well.


async function fetchPythonPackageMetadata(name: string, registry: string) {
// Fetch package metadata from PyPI JSON API
const metadataResponse = await fetchWithTimeout(`${registry}/${name}/json`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a legacy API endpoint that only PyPI uses. There is a PEP 691 which is a standard now, so I would recommend to use that instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it easy to move to the new endpoints? Like is it just a URL change? If not we can do this in a follow up (so a TODO comment is fine here IMO)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did end up starting this one on a separate branch, will add a todo

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little bit of URL change + json handling. You can start with this legacy api and switch to the PEP 691 one in a follow up PR (note that the Pyodide index (index.pyodide.org) does not support this endpoint)

Comment thread packages/worker-bundler/src/installer.ts
Comment thread packages/worker-bundler/src/installer.ts

@abstractedfox abstractedfox left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have some more commits in for changes requested by @ryanking13, though I may want to save some of them for a future PR if that's alright

Comment thread packages/worker-bundler/src/tests/wrangler.jsonc Outdated
Comment thread packages/worker-bundler/src/index.ts Outdated
mainModule: entryPoint,
modules,
wranglerConfig: {
compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recall just choosing it as an arbitrary date that I thought would work, I can put a newer one. The JS worker as in the worker that creates the dynamic worker?

Comment thread packages/worker-bundler/src/index.ts Outdated
modules,
wranglerConfig: {
compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",
compatibilityFlags: wranglerConfig?.compatibilityFlags ?? [

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in to the user-defined compatibility flags, in the event the user passed flags that didn't include python_workers? I can add that

*/

import * as semver from "semver";
import { unzipSync } from "fflate";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually only did it this way because I dryly assumed I'd need a lib for it, I'd much rather save a dependency so I'm glad you told me that

installedPackages,
inProgress,
registry
PYPI_JSON_API // hardcoding this for now to keep the implementation light

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh thanks, that actually answers a question that was on my mind this morning. I may want to do that in an upcoming pr if that's alright with you

Comment thread packages/worker-bundler/src/installer.ts
}

// Skip if the package already exists in the filesystem
if (fileSystem.read(`python_modules/${name}/__init__.py`) !== null) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, thanks for pointing that out, I realized distribution packages could have differently named packages inside them while working in another branch but I didn't change this there either (though I didn't know packages could not have __init__.py). I more or less did what the JS implementation was doing here, a snap assumption was that it was to prevent races in some way but the comment for the JS one says it's done in case installDependencies is called on a 'pre-warmed filesystem', though I don't know how often this actually happens

const response = await fetchWithTimeout(
wheelUrl,
{},
DEFAULT_TIMEOUT_MS * 2

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly because it's not clear to me whether that would have undesirable effects elsewhere, there are other places where this function is called with the default duration. I ended up just staying with what the JS one was doing

`No wheel distribution found for ${name}@${version} on PyPI`
);
}
const wheelUrl = wheel.url;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thank you, I was thinking earlier about what I'd need to do about platform specific wheels

Comment thread packages/worker-bundler/src/installer.ts

@dom96 dom96 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments and things that I think should be fixed before merge, but overall looks good to me so approving.

Comment thread packages/worker-bundler/src/tests/e2e.test.ts Outdated
Comment thread packages/worker-bundler/src/tests/e2e.test.ts Outdated
Comment thread packages/worker-bundler/src/index.ts Outdated
Comment thread packages/worker-bundler/src/index.ts Outdated
modules,
wranglerConfig: {
compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01",
compatibilityFlags: wranglerConfig?.compatibilityFlags ?? [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

Comment on lines +149 to +151
if (path === "pyproject.toml") {
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? No one is likely to be specifying pyproject.toml in the createWorker call

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dom96 This is the pyproject.toml that contains all the deps for the worker, so as far as I understand we do need to have it coming into createWorker

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, my mistake, for some reason I thought pyproject.toml wasn't in the "files" dict, but one level above (same for packages.json, but I just checked and it's also in the "files" dict).

All good to leave this then.

Comment thread packages/worker-bundler/src/installer.ts
}

// Skip if the package already exists in the filesystem
if (fileSystem.read(`python_modules/${name}/__init__.py`) !== null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With @ryanking13's comment below, perhaps the best thing to do here is to just delete this and rely on the installedPackages check instead. You can add a TODO to consider doing this in the future because the JS is doing it.

}

// Skip if the package already exists in the filesystem
if (fileSystem.read(`python_modules/${name}/__init__.py`) !== null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`No wheel distribution found for ${name}@${version} on PyPI`
);
}
const wheelUrl = wheel.url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this kind of thing can be pushed to a TODO comment and done in separate PRs. Let's iterate via follow up PRs rather than ensuring everything is 100% in this PR, especially when it comes to Python packaging details.


async function fetchPythonPackageMetadata(name: string, registry: string) {
// Fetch package metadata from PyPI JSON API
const metadataResponse = await fetchWithTimeout(`${registry}/${name}/json`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it easy to move to the new endpoints? Like is it just a URL change? If not we can do this in a follow up (so a TODO comment is fine here IMO)

@abstractedfox abstractedfox left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last question in the comments but I think this is otherwise just about there!

Comment on lines +149 to +151
if (path === "pyproject.toml") {
continue;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dom96 This is the pyproject.toml that contains all the deps for the worker, so as far as I understand we do need to have it coming into createWorker


async function fetchPythonPackageMetadata(name: string, registry: string) {
// Fetch package metadata from PyPI JSON API
const metadataResponse = await fetchWithTimeout(`${registry}/${name}/json`);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did end up starting this one on a separate branch, will add a todo

@ryanking13

Copy link
Copy Markdown

cc: @threepointone The python workers team doesn't seem to be possible to approve and merge this. Can you help with this?

@xcaptain

Copy link
Copy Markdown

So if this PR got merged, dynamic worker can be a full featured python code runner, no need to use docker based sandbox right? I don't like the docker sandbox because it's imcompatitable with vitest-pool-workers which make writing tests very hard (need to start real server to connect to docker).

@ryanking13

Copy link
Copy Markdown

@xcaptain This is still experimental so the answer is yes and no. I would say you should not rely on this yet (or you can volunteer to help us debugging things :) ).

no need to use docker based sandbox right? I don't like the docker sandbox because it's imcompatitable with cloudflare/workers-sdk#10408 which make writing tests very hard (need to start real server to connect to docker).

Also, yes and no. It is true that you can use dynamic workers with Python, but it you need to run a workload that is computation / memory heavy, then it is not a good fit for worker. So it depends on your workload

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants