Open-source project
amol-/dukpy avatar
amol-/dukpy

dukpy: running JavaScript from Python with an embedded QuickJS

Simple JavaScript interpreter for Python

534 stars42 forksCMIT

At a glance

What is it?
dukpy is a Python package that embeds the QuickJS engine so you can evaluate JavaScript, run .mjs and .cjs files, pass arguments in and get JSON-compatible values back, and it also bundles TypeScript, JSX and LESS compilers for transpiling. It suits a Python codebase that has to run or compile JavaScript without a Node install, and it is not a browser-grade JavaScript runtime.
Who is it for?
dukpy fits a Python service that needs to evaluate JavaScript or transpile a front end asset without shipping Node, since the QuickJS engine is compiled into the extension module and the transpilers are bundled, and since the JSON-only boundary keeps the integration predictable. It is the wrong choice for a browser target, since QuickJS is not a browser engine and there is no DOM, and it is the wrong choice if you need npm packages, since there is no package manager here.
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 92 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

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

Editorial analysis

What the package actually embeds

dukpy is described as a simple JavaScript interpreter for Python without any external runtime dependency. The name comes from dukpy's original Duktape-based implementation and is kept for package compatibility.

That last sentence matters more than it sounds, because the engine has changed underneath. The current implementation is QuickJS, not Duktape, and the repository layout shows why. There is a src/ directory containing quickjs/quickjs-amalgam.c, _support.c and dukpyjs.c, and the setup.py builds a single C extension from those three files.

The build configuration is careful about platforms. On Windows it uses /std:c11 and /experimental:c11atomics, with a comment explaining that MSVC keeps __STDC_NO_ATOMICS__ defined even with experimental C11 atomics enabled, so CONFIG_ATOMICS is defined explicitly. On Linux it uses -std=c11 and defines _GNU_SOURCE, and on other platforms it uses -std=c11 with no extra macros. The extension is named dukpy._dukpy, and the package data excludes the built shared libraries so they are not shipped as package data.

The payoff for all of that is the phrase without any external runtime dependency. There is no Node binary to install, no npm install step, and no subprocess. QuickJS is compiled into the extension module and the JavaScript engine is right there inside your Python process.

The project metadata describes the package as dukpy at version 0.6.0, requiring Python 3.9 or newer, MIT licensed, with classifiers for Python 3.9 through 3.14 and Development Status 3 - Alpha. The primary language of the repository is C. The last push was on 2026-07-23, and the previous releases were 0.5.1 in February 2026 and 0.5.0 in November 2024.

Evaluating code and passing values across the boundary

The basic call is dukpy.evaljs with the source text, and the return value is the result encoded as far as possible into JSON.

python
>>> import dukpy
>>> dukpy.evaljs("var o = {'value': 5}; o['value'] += 3; o")
{'value': 8}

If execution fails, a dukpy.JSRuntimeError exception is raised with the failure reason. That is the only error type the readme names, and the existence of a specific exception rather than a generic one means you can catch interpreter failures separately from Python-side errors.

Arguments go in the other direction through the dukpy object inside JavaScript, and the constraint is that an argument must be encodable as JSON to be available:

python
>>> import dukpy
>>>
>>> def sum3(value):
...     return dukpy.evaljs("dukpy['value'] + 3", value=value)
...
>>> sum3(7)
10

The evaljs function can also take several sources to execute in the same context, passed as a list or a tuple:

python
>>> import dukpy
>>> dukpy.evaljs(["var o = {'value': 5}",
...               "o['value'] += 3",
...               "o"])
{'value': 8}

That is the pattern for when your code has dependencies, loading the dependency first and then your code. It is also the closest thing to a session, though the readme notes that evaljs creates a new interpreter on each call, which is usually convenient and avoids errors from dirty global variables or unexpected state. A persistent interpreter is available separately through JSInterpreter, and the readme also mentions dukpy.run for running a JavaScript file entrypoint.

The persistent interpreter and the file runner are two different tools, and the readme is explicit about the difference between them. evaljs always evaluates source text as a script. run reads a file and classifies its entrypoint.

How run classifies .mjs, .cjs and .js files

The module classification rules are the part of dukpy that deserves reading carefully, because they are unusual and the readme is unusually explicit about what the implementation does not do.

.mjs files are native ES modules. .cjs files are CommonJS. .js files follow the nearest package.json type of module or commonjs. A .js file without that package metadata is ambiguous, and the resolution is a fallback rather than a heuristic scan.

For both entrypoints and dependencies, dukpy asks QuickJS to compile the CommonJS wrapper first, and if that fails it runs the file as a native ES module. The readme states plainly what dukpy never does: it never scans source text for import, export, await, comments, strings, or identifiers.

That is a good design decision to know about, because the alternative implementations get it wrong. Sniffing source text for the word import means a string containing that word can change how your file is classified, and it means a file using await at the top level is misread. Trying the CommonJS wrapper and falling back is a decision based on what actually compiles.

The consequences of the classification are also documented. Entrypoints use the loader's canonical module ID, so for a file under a registered loader path, import.meta.url and CommonJS module.id may be relative to that path. And when ES modules import files classified as CommonJS, dukpy exposes a minimal synthetic namespace: default is module.exports, and the only named exports are module, exports and require. Dukpy does not infer named exports from CommonJS source, so the readme advises using a default import for CommonJS API objects.

The entrypoint examples are short. dukpy.run takes a path:

python
>>> import dukpy
>>> dukpy.run("./main.mjs")
{}

and the empty dictionary is the JSON-encoded return value of a module with nothing to return.

Three transpilers bundled in

What distinguishes dukpy from a bare JavaScript interpreter is that it ships three compilers, and the readme gives each one a function and a WebAssets filter.

The TypeScript compiler is available through dukpy.typescript_compile, and the example shows a class with a constructor parameter property and a method being compiled to ES5-style output. The readme notes the compiler has built-in options and does not accept additional ones, which is a real limitation for anyone who needs a specific target or a custom compiler flag.

There is also a WebAssets filter, the Python asset pipeline library, for each compiler. You register it under a name:

python
from webassets.filter import register_filter
from dukpy.webassets import TypeScript

register_filter(TypeScript)

which makes the filter available with the typescript name. The JSX filter is registered as BabelJSX and the LESS filter as CompileLess with the name lessc, following the same three-line pattern.

The JSX compiler is available as dukpy.jsx_compile and compiles JSX to React.createElement calls. The LESS compiler is available as dukpy.less_compile, which evaluates a LESS stylesheet and returns the compiled CSS.

One operational note on the TypeScript output: the readme warns that when using the compiler for code that needs to run in the browser, you must add the SystemJS dependency, because import statements are resolved using SystemJS. That is a consequence of compiling to CommonJS-style modules, and it means the TypeScript path and the ES module path are not interchangeable in a browser.

The JSX filter also has a BABEL_MODULES_LOADER option that accepts systemjs or umd to compile modules for SystemJS or UMD instead of CommonJS, which is the knob to reach for when the output has to run in a browser without a module loader.

The package manifest lists webassets as an optional dependency, and the webassets filter modules are packaged as dukpy.webassets.

Where dukpy is the wrong choice

The first limitation is that this is not a browser. QuickJS is a small, fast embeddable engine, and it is not a browser engine. There is no DOM, no window, no document, no fetch, no event loop with timers and no layout. Any JavaScript you run here has to be written to run in a bare engine, which rules out most front end library code and rules out every library that expects a browser global.

The second is that there is no package manager. The readme does not mention npm, node_modules or a module resolution scheme beyond the loader paths it registers. So the ecosystem you can use is whatever you vendor yourself, and the ability to run a popular package is not part of the design. That is a reasonable trade for an embedded engine, and it is a hard limit for anyone hoping to reuse an existing JavaScript library.

The third is the JSON boundary. Arguments must be encodable as JSON and results are returned as far as possible encoded as JSON. That rules out passing functions, class instances, cyclic structures and typed arrays across the boundary, and it means a rich return value becomes a plain object on the Python side. For a scripting interface that is the right simplicity, and for anything structured it is a constraint.

The fourth is the version state. The package is at 0.6.0, classified as Development Status 3 - Alpha, and the engine underneath has been swapped from Duktape to QuickJS while the package name stayed the same for compatibility. That means the name tells you nothing about the engine, and any documentation, blog post or issue you find about dukpy under the Duktape name may describe behaviour that no longer holds. The readme's classification of Alpha is the honest signal here, and it should be weighed accordingly.

The fifth is the transpiler limitations. The TypeScript compiler has built-in options and does not accept additional ones, so a project that needs a specific target, a specific module format or a plugin will not get it. And the browser story for the TypeScript output requires adding SystemJS by hand, which is friction that a real build tool would not impose.

Editorial conclusion

dukpy fits a Python service that needs to evaluate JavaScript or transpile a front end asset without shipping Node, since the QuickJS engine is compiled into the extension module and the transpilers are bundled, and since the JSON-only boundary keeps the integration predictable. It is the wrong choice for a browser target, since QuickJS is not a browser engine and there is no DOM, and it is the wrong choice if you need npm packages, since there is no package manager here. Before adopting it, note that the release is classified as Alpha at version 0.6.0, that the TypeScript compiler is documented as not accepting additional options, and that the last push was on 2026-07-23 with 0.6.0 released the same day.

Frequently asked questions

What is dukpy?

dukpy is a simple JavaScript interpreter for Python without any external runtime dependency. It embeds the QuickJS engine as a compiled C extension, and the name is kept from the original Duktape-based implementation for package compatibility. The package is MIT licensed and at version 0.6.0.

How do I run JavaScript from Python with dukpy?

Call dukpy.evaljs with the source text, which returns the result as far as it can be encoded into JSON, and raises dukpy.JSRuntimeError if execution fails. For a file, use dukpy.run with a path, or JSInterpreter.run when you want to reuse an interpreter.

How does dukpy decide whether a JavaScript file is a module?

.mjs files are native ES modules, .cjs files are CommonJS, and .js files follow the nearest package.json type. When that metadata is missing, dukpy asks QuickJS to compile the CommonJS wrapper first and falls back to running the file as a native ES module, and it never scans source text for import, export or await.

Which compilers does dukpy include?

TypeScript through dukpy.typescript_compile, JSX to React through dukpy.jsx_compile, and LESS through dukpy.less_compile. Each also has a WebAssets filter, registered as typescript, babeljsx and lessc respectively.

Can I pass values into JavaScript running under dukpy?

Yes, as keyword arguments to evaljs, where they are available inside JavaScript in the dukpy object, for example dukpy['value']. The arguments must be encodable as JSON for them to be available in JavaScript.

Official sources

  1. amol-/dukpy on GitHub
  2. Issues
  3. License: MIT
  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/amol-dukpy.svg)](https://hysenlabs.com/projects/amol-dukpy)

Was this useful?