Open-source project
denosaurs/deno_python avatar
denosaurs/deno_python

deno_python: calling Python from Deno and Bun through the C API

🐍 Python interpreter bindings for Deno and Bun.

758 stars27 forksTypeScriptMIT

At a glance

What is it?
deno_python is a TypeScript module that embeds the Python interpreter through its C API using FFI, so a Deno or Bun program can import and call any locally installed Python package natively. It needs a Python installation with a shared library and it runs with FFI permissions enabled.
Who is it for?
deno_python suits a team that wants to call an existing Python package from a TypeScript service without running two processes or a WebAssembly Python, since the bridge uses the real interpreter and the real site-packages. It does not suit a deployment target without a Python shared library, and the FFI permission requirement is a genuine sandbox weakening.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 39 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

How the bridge reaches Python

deno_python provides a direct integration between JavaScript in Deno or Bun and Python by integrating with the Python C API. The README describes it as a bridge between the two languages, enabling you to pass data and execute Python code from within your JavaScript applications.

The mechanism is FFI. The module calls into the Python interpreter's shared library directly, which is why the runtime permissions matter and why the project sits in the FFI topic list alongside cpython, python-c-api, deno-ffi and bun-ffi.

The advantage the README puts at the centre is that this stays native, unlike a runtime like pyodide which is compiled to WebAssembly, sandboxed and may not work with all Python packages. The bridge uses the existing Python installation, so the package you import is the one already installed on the machine, with the versions and the native wheels that implies. That is the whole argument for the approach: a Python package that needs a compiled extension works, because there is no WebAssembly port of it to worry about.

The cost is the mirror image. The JavaScript runtime is no longer sandboxed from the system, because FFI is by definition an escape hatch, and the Python interpreter has to exist on the machine. This is a tool for a server, not for a browser or an edge isolate.

The repository is under the MIT licence, with the LICENSE file at the root, and the code is TypeScript with a Bun entry point at mod.bun.ts and a Deno entry point at mod.ts. The package name on npm is bunpy.

Importing Python packages from a Deno script

The README's example imports a locally installed package, matplotlib, through the deno.land module URL.

ts
import { python } from "https://deno.land/x/python/mod.ts";

const np = python.import("numpy");
const plt = python.import("matplotlib.pyplot");

const xpoints = np.array([1, 8]);
const ypoints = np.array([3, 10]);

plt.plot(xpoints, ypoints);
plt.show();

The API is a direct mirror of Python's own import machinery. python.import takes a module path, the dotted form matplotlib.pyplot included, and returns a proxy you call methods on. The array construction goes through numpy in Python, the plotting call is a Python method call, and the plot window appears because matplotlib is running as a real Python process with a real display.

The permission flags are not optional. The README says you must specify --allow-ffi, --allow-env and --unstable-ffi when running, and notes that you may instead specify -A, since enabling FFI effectively escapes the permissions sandbox. That is a direct statement of the security trade-off, and it is worth taking seriously: an allow-ffi permission in a Deno service is a grant of arbitrary native code execution, so the process should be treated as trusted code.

shell
deno run -A --unstable-ffi <file>

The --unstable-ffi flag is there because the FFI API in Deno has not been stabilised, so a Deno upgrade can in principle change the surface this module depends on.

The same bridge under Bun

Bun is supported through the bunpy package on npm, and the API is the same, only the import specifier changes.

ts
import { python } from "bunpy";

const np = python.import("numpy");
const plt = python.import("matplotlib.pyplot");

const xpoints = np.array([1, 8]);
const ypoints = np.array([3, 10]);

plt.plot(xpoints, ypoints);
plt.show();

The package.json in the repository is the Bun manifest, and it names the package bunpy at version 0.4.6 with main set to mod.bun.ts, and it ships the Deno files too, mod.ts, ext/pip.ts, plugin.ts and the src/ modules for FFI, the Python bridge, symbols and utilities.

The test script is worth noticing, because it runs both runtimes.

json
    "test": "deno task test && bun test"

Running the Deno task and the Bun test suite in sequence is a simple but effective way to keep the two entry points honest with each other, since the Deno and Bun paths share most of the source but differ in how they load the FFI symbols and the plugin.

The repository also has a bunfig.toml, a deno.json, an ipy.ts at the root, and an ext/ directory for the pip utility, plus an examples/ directory with hello_python.ts, import.js, matplotlib.ts, pip_import.ts, run_code.ts, tensorflow.ts and a test.py.

Dependency management with ext/pip

The default behaviour of deno_python follows the normal Python way of resolving imports, going through sys.path and resolving them globally, locally or scoped to a virtual environment. The README calls this great and then immediately says it may not be a good thing for a Deno module which depends on a Python package.

The reason is specific. If a Deno module needs a Python package, and that package is installed into the global Python scope, then two unrelated projects can require conflicting versions of the same library, and the global site-packages becomes a shared mutable resource that nobody declared. The fix is scoped installs.

That is what the ext/pip utility exists for. It allows you to install Python dependencies using pip, scoped to either the global Deno installation or to a location defined by the --location passed to Deno, without leaking to the global Python scope. The README says it uses the same caching location and algorithm as plug and deno cache, so the packages land in a Deno-managed cache rather than in site-packages.

Using it is a matter of calling the import or install methods, and the rest is handled automatically.

ts
import { pip } from "https://deno.land/x/python/ext/pip.ts";

const np = await pip.import("numpy");
const plt = await pip.import("matplotlib", "matplotlib.pyplot");

const xpoints = np.array([1, 8]);
const ypoints = np.array([3, 10]);

plt.plot(xpoints, ypoints);
plt.show();

The two calls are awaited, which is the visible difference from the plain python.import form, because the scoped variant has to install or locate the package first. The second call takes two module names and returns the second, which is how you get a submodule handle directly.

The dependency table also has a side benefit for Deno users. Because the packages are cached the way Deno caches its own modules, a Deno project gets lockfile-style reproducibility for its Python side, which plain pip does not give you.

Finding the interpreter and running it in Docker

The module uses FFI to interface with the Python interpreter's C API, so you must have an existing Python installation with the shared library, something like python310.dll. The README states plainly that Python installed from the Microsoft Store does not work, because it does not contain the shared library needed for interfacing with the interpreter. That is a Windows-specific trap and it is the first thing to check if the module cannot find Python.

If the module fails to find Python, you can add the path in the DENO_PYTHON_PATH environment variable, and the README is specific about what that value must be: a full path including the file name of the Python dynamic library, which is like python310.dll on Windows, libpython310.dylib on macOS and libpython310.so on Linux depending on platform. So the value points at the library file, not at a directory.

The Docker path uses the denoland/deno:bin image along with the official python image, and the example in the README composes them with a build argument for each version.

dockerfile
ARG DENO_VERSION=1.38.2
ARG PYTHON_VERSION=3.12

FROM denoland/deno:bin-$DENO_VERSION AS deno
FROM python:$PYTHON_VERSION

# Copy and configure deno
COPY --from=deno /deno /usr/local/bin/deno
ENTRYPOINT ["/usr/local/bin/deno"]

# Copy your project source
COPY . .

RUN ["run", "-A", "--unstable", "https://deno.land/x/[email protected]/examples/hello_python.ts"]

The pattern is the interesting part. The Deno binary is copied out of its own image into the Python image, so the final container has both a Python installation with its shared library and a Deno runtime. The entrypoint is Deno, and the run command uses the -A flag and the unstable flag rather than naming the ffi permission, since -A is already the shorthand for full permissions.

Note that the pinned Deno version in the example, 1.38.2, and the pinned python module version, 0.4.2, are older than the current releases, which tells you the example was written against what existed at the time and has not been refreshed.

Where deno_python is the wrong choice

The first limitation is the sandbox. The README says it plainly, that enabling FFI effectively escapes the permissions sandbox, and the recommended invocation is -A. So a service built on this bridge has none of the isolation that is the main reason to run on Deno in the first place. A Deno program that imports Python has the same access to the machine as a Node program that shells out to it, and in exchange you get a nicer calling convention. If the reason you chose Deno was the permission model, this defeats it.

The second is that a Python installation is now a deployment dependency. The container needs both a Deno binary and a Python with a shared library, and the version of Python becomes part of your compatibility matrix, because a package that needs a specific Python version will now constrain your runtime. That is a heavier operational story than a single static binary.

The third is the Windows Store Python trap, which the README calls out. A Python installation that lacks the shared library silently does not work with this module, and a developer on a fresh Windows machine will hit it.

The fourth is the API stability question. The Deno FFI surface is still behind --unstable-ffi, and the module depends on it, so a Deno upgrade is a risk rather than a routine bump. The same applies on the Bun side, where the plugin mechanism is Bun-specific.

The fifth is the version cadence. The listed releases are 0.4.6 from October 2025, 0.4.5 from June 2025 and 0.4.4 from February 2025, so the project releases a few times a year, and the last push was on 2026-09-16. The module is pre-1.0, and the Docker example in the README still pins 0.4.2, so the documentation and the current release are not in step.

Licence, maintenance and the cost of two runtimes in one process

The project is under the MIT licence, with the LICENSE file at the root of the repository, and the README carries the copyright lines for DjDeveloperr in 2021 and the Denosaurs team in 2023.

The repository is not archived and the last push was on 2026-09-16, so the bridge is being maintained. The release history is 0.4.6 from October 2025, 0.4.5 from June 2025 and 0.4.4 from February 2025, so roughly twice a year. The maintainers are named as DjDeveloperr and Elias Sjögreen, and the contribution conventions are stated: code style is formatted with deno fmt and commit messages follow the Conventional Commits specification.

The cost of running this is the operational weight of two runtimes in one process. The image needs Deno, Python and the Python shared library, and the container is built with a Deno binary copied out of a separate image into the Python base, which is a slightly unusual construction that you have to maintain as the base images change.

The cost of debugging it is the FFI boundary. A failure in a Python call surfaces in JavaScript as an opaque error from the interpreter, and a failure in the bridge itself surfaces as a symbol lookup problem, and neither tells you which side failed. The examples directory includes a run_code.ts and a tensorflow.ts, and the tensorflow example is the practical test of whether the bridge handles a package with heavy native dependencies, since that is the case the whole approach exists to serve.

The upside is that the Python side is ordinary Python. Whatever the package does, it does, and the maintenance burden of the Python half is the same as it would be in a Python project.

Editorial conclusion

deno_python suits a team that wants to call an existing Python package from a TypeScript service without running two processes or a WebAssembly Python, since the bridge uses the real interpreter and the real site-packages. It does not suit a deployment target without a Python shared library, and the FFI permission requirement is a genuine sandbox weakening. Verify the Python installation first, since the Microsoft Store build of Python does not work and the path must be set in DENO_PYTHON_PATH if it is not found.

Frequently asked questions

How do I run a deno_python script?

The README says you must specify --allow-ffi, --allow-env and --unstable-ffi, and you may use -A instead of the individual permissions since enabling FFI effectively escapes the permissions sandbox. The example invocation is deno run -A --unstable-ffi with the file path.

Why can deno_python not find my Python installation?

Python installed from the Microsoft Store does not work because it does not contain the shared library needed for interfacing with the interpreter. You can point the module at the library with the DENO_PYTHON_PATH environment variable, which must be a full path including the file name, such as libpython310.so on Linux or libpython310.dylib on macOS.

How do I install Python packages for a deno_python project without polluting the global Python?

Use the ext/pip utility, which installs with pip scoped to the global Deno installation or to a location defined by the --location passed to Deno, using the same caching as plug and deno cache. The import and install methods handle the rest, and the calls are awaited.

Can I use deno_python with Bun?

Yes, through the bunpy package on npm, whose entry point is mod.bun.ts. The API is the same, with python.import calls against the locally installed packages, and the test script runs both deno task test and bun test so the two paths stay in step.

Official sources

  1. denosaurs/deno_python on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/denosaurs-deno-python.svg)](https://hysenlabs.com/projects/denosaurs-deno-python)

Was this useful?