UlionTse/exejs

Run JavaScript code from Python.「髯祭司」是一个旨在使用python运行javascript的库。

Python

145

17 commits

updated Jun 28, 2026

See the code

README

ExeJS

Run JavaScript from Python — the modern successor to PyExecJS.

Zero dependencies · Multi-runtime auto-detection · Async-ready · No temp files · Timeout guard

PyPI - Version Conda - Version PyPI - Python PyPI - License PyPI - Status PyPI - Wheel PyPI - Downloads


Table of Contents

Features

  • Multi-runtime — auto-detects among Node, JavaScriptCore, SpiderMonkey, JScript, PhantomJS, SlimerJS, and Nashorn.
  • Zero dependencies — built on the Python standard library; pip install exejs and you're done.
  • Async-ready — evaluate_async / execute_async / call_async built on asyncio.
  • No temp files — compiles and runs via stdin (except JScript), so it won't trigger antivirus alerts.
  • Timeout guard — timeout parameter kills runaway scripts instead of hanging forever.
  • Structured args — call() auto JSON-serializes objects and supports property paths like calc.mul.
  • Typed — ships py.typed, fully annotated, Python 3.9+.

Why ExeJS

PyExecJS has been EOL since 2018. ExeJS exists to fix three concrete pain points:

  1. PyExecJS is unmaintained, and downstream builds that depend on it fail or get cancelled. (Issue#1, Issue#2)
  2. PyExecJS writes compiled code to a temp file by default, which triggers antivirus alerts and blocks execution. (Issue#3)
  3. No async API, no timeout control, still carries Python 2 baggage.

Comparison

PyExecJS (EOL 2018)ExeJS
Maintainedstopped 2018active
Python 2yesno (3.9+)
Async API—yes
Temp filesyes (antivirus alerts)no (stdin, except JScript)
Timeout—yes
Context reuse (compile/call)limitedyes
Structured args (auto JSON, property paths)—yes
Dependenciessome (six)zero
Typed (py.typed)—yes

Requirements

  • Python >= 3.9
  • A JavaScript runtime installed and on PATH — Node.js is the recommended default. See Supported Runtimes for the full list and install hints.

Installation

# PyPI
pip install --upgrade exejs

# Conda
conda install conda-forge::exejs

# From source
git clone https://github.com/UlionTse/exejs.git
cd exejs
pip install .

Quick Start

import exejs

exejs.evaluate("[1, 2, 3].map(x => x * 2)")  # [2, 4, 6]

# evaluate (one-shot expression)
exejs.evaluate("'red yellow blue'.split(' ')")  # ['red', 'yellow', 'blue']

# execute (one-shot statements; use `return` to send a value back)
exejs.execute('var x = 40 + 2; return x;')  # 42

For context reuse, structured args, async, and timeout, see Advanced.

Supported Runtimes

RuntimeCommandEngineInstall / Notes
NodenodeChrome (V8)recommended, all platforms — nodejs.org/download
NodeJSnodejsChrome (V8)Debian/Ubuntu alias of Node
JavaScriptCorejscSafari (WebKit)macOS only (bundled)
SpiderMonkeyjsFirefox (Gecko)js package on most Linux distros
PhantomjsphantomjsWebKitupstream abandoned 2018
SlimerJSslimerjsGeckoupstream abandoned 2021
NashornjjsJava (JVM)Oracle JDK; removed since JDK 15
JScriptcscript //E:jscript //NologoIE (Trident)Windows only (built-in); uses temp file

ExeJS auto-detects the first available runtime in the order above. Use reset_runtime to pick one explicitly.

API Reference

Top-level functions

exejs.evaluate(source: str, timeout: float | None = None) -> Any
exejs.execute(source: str, timeout: float | None = None) -> Any
exejs.compile(source: str = '', cwd: str | None = None) -> RuntimeCompileContext

async exejs.evaluate_async(source: str, timeout: float | None = None) -> Any
async exejs.execute_async(source: str, timeout: float | None = None) -> Any

Compile context

ctx = exejs.compile(source='', cwd=None)

ctx.call(key: str, *args, timeout: float | None = None) -> Any        # key supports paths like 'calc.mul'
ctx.evaluate(source: str, timeout: float | None = None) -> Any
ctx.execute(source: str, timeout: float | None = None) -> Any

async ctx.call_async(key: str, *args, timeout: float | None = None) -> Any
async ctx.evaluate_async(source: str, timeout: float | None = None) -> Any
async ctx.execute_async(source: str, timeout: float | None = None) -> Any

Runtime management

exejs.reset_runtime(name: str) -> None                       # switch runtime by name
exejs.get_current_runtime() -> Runtime
exejs.get_current_runtime_name() -> str
exejs.find_available_runtime() -> Runtime                    # first available
exejs.find_all_runtime_name_list(is_available: bool = True) -> list[str]

Exception hierarchy

ExejsError
├── ExejsRuntimeNameError          # unknown runtime name passed to reset_runtime
├── ExejsRuntimeUnavailableError   # no runtime found / runtime not on PATH
├── ExejsProcessExitError          # subprocess failed / non-zero exit
├── ExejsProgramError              # JS crashed or returned invalid JSON
└── ExejsTimeoutError              # timeout exceeded

Advanced

Compile & call (context reuse)

Reuse a single JS context across multiple calls instead of re-launching a runtime each time:

import exejs

ctx = exejs.compile('function add(x, y) { return x + y; }')
ctx.call('add', 1, 2)  # 3
ctx.call('add', 10, 20)  # 30

Structured arguments

call() auto JSON-serializes arguments and supports property paths as the key:

# call an object method (key may be a property path)
ctx = exejs.compile('var calc = { mul: function(a, b) { return a * b; } };')
ctx.call('calc.mul', 3, 4)  # 12

# call with a dict argument (auto JSON-serialized)
ctx = exejs.compile('function greet(u) { return "hi " + u.name + ", age " + u.age; }')
ctx.call('greet', {'name': 'Tom', 'age': 18})  # 'hi Tom, age 18'

Async API

Async is useful inside async web frameworks (FastAPI, aiohttp) or when you want to avoid blocking the event loop:

import asyncio
import exejs

asyncio.run(exejs.evaluate_async("'red yellow blue'.split(' ')"))

Timeout

timeout (seconds) applies to both sync and async variants. On expiry the subprocess is killed and ExejsTimeoutError is raised:

try:
    exejs.execute('while (true) {}', timeout=2.0)
except exejs.ExejsTimeoutError as e:
    print('killed:', e)

Choosing a runtime

import exejs

# see what's available on this machine
print(exejs.find_all_runtime_name_list())  # e.g. ['Node', 'JScript']

# force a specific one
exejs.reset_runtime('Node')
print(exejs.get_current_runtime_name())  # 'Node'

Working directory

compile(source, cwd=...) sets the subprocess working directory, useful when your JS reads relative files:

ctx = exejs.compile('return require("./config.json")', cwd='/path/to/project')
ctx.evaluate('')

FAQ

  • ExejsRuntimeUnavailableError: Unable to find available javascript runtime — No JS runtime is installed or none is on PATH. Install Node.js from nodejs.org and reopen your terminal, or call exejs.reset_runtime('JScript') on Windows where cscript is built in.
  • Antivirus blocks execution — If you are using the JScript runtime (Windows), it still writes a temp file which may trigger antivirus. Switch to Node (exejs.reset_runtime('Node')) to avoid this. All other runtimes use stdin and should not be affected.
  • cscript not recognized on Windows — JScript is provided by Windows Script Host. Ensure cscript.exe exists (usually at C:\Windows\System32\) and is on PATH; on stripped-down Windows images you may need to enable the Windows Script Host feature.
  • Blocks the event loop in asyncio code — The sync evaluate / execute block the calling thread. Inside an async framework use evaluate_async / execute_async instead so the event loop stays responsive.

Changelog

See change_log.md.

Contributing

Pull requests are welcome. To set up a dev environment:

git clone https://github.com/UlionTse/exejs.git
cd exejs
pip install -e .
pip install pytest
pytest exejs/tests

Run pytest exejs/tests before submitting a PR. Code style follows PEP 8. Please open an issue first to discuss any non-trivial change.

Acknowledgements

Notable Usage

ExeJS is a core dependency of Translators, where it handles the JavaScript execution layer that translation engines rely on.

License

Apache-2.0 © UlionTse

javascript

UlionTse/exejs

Run JavaScript code from Python.「髯祭司」是一个旨在使用python运行javascript的库。

Python

145

17 commits

updated Jun 28, 2026

See the code

README

ExeJS

Run JavaScript from Python — the modern successor to PyExecJS.

Zero dependencies · Multi-runtime auto-detection · Async-ready · No temp files · Timeout guard

PyPI - Version Conda - Version PyPI - Python PyPI - License PyPI - Status PyPI - Wheel PyPI - Downloads


Table of Contents

Features

  • Multi-runtime — auto-detects among Node, JavaScriptCore, SpiderMonkey, JScript, PhantomJS, SlimerJS, and Nashorn.
  • Zero dependencies — built on the Python standard library; pip install exejs and you're done.
  • Async-ready — evaluate_async / execute_async / call_async built on asyncio.
  • No temp files — compiles and runs via stdin (except JScript), so it won't trigger antivirus alerts.
  • Timeout guard — timeout parameter kills runaway scripts instead of hanging forever.
  • Structured args — call() auto JSON-serializes objects and supports property paths like calc.mul.
  • Typed — ships py.typed, fully annotated, Python 3.9+.

Why ExeJS

PyExecJS has been EOL since 2018. ExeJS exists to fix three concrete pain points:

  1. PyExecJS is unmaintained, and downstream builds that depend on it fail or get cancelled. (Issue#1, Issue#2)
  2. PyExecJS writes compiled code to a temp file by default, which triggers antivirus alerts and blocks execution. (Issue#3)
  3. No async API, no timeout control, still carries Python 2 baggage.

Comparison

PyExecJS (EOL 2018)ExeJS
Maintainedstopped 2018active
Python 2yesno (3.9+)
Async API—yes
Temp filesyes (antivirus alerts)no (stdin, except JScript)
Timeout—yes
Context reuse (compile/call)limitedyes
Structured args (auto JSON, property paths)—yes
Dependenciessome (six)zero
Typed (py.typed)—yes

Requirements

  • Python >= 3.9
  • A JavaScript runtime installed and on PATH — Node.js is the recommended default. See Supported Runtimes for the full list and install hints.

Installation

# PyPI
pip install --upgrade exejs

# Conda
conda install conda-forge::exejs

# From source
git clone https://github.com/UlionTse/exejs.git
cd exejs
pip install .

Quick Start

import exejs

exejs.evaluate("[1, 2, 3].map(x => x * 2)")  # [2, 4, 6]

# evaluate (one-shot expression)
exejs.evaluate("'red yellow blue'.split(' ')")  # ['red', 'yellow', 'blue']

# execute (one-shot statements; use `return` to send a value back)
exejs.execute('var x = 40 + 2; return x;')  # 42

For context reuse, structured args, async, and timeout, see Advanced.

Supported Runtimes

RuntimeCommandEngineInstall / Notes
NodenodeChrome (V8)recommended, all platforms — nodejs.org/download
NodeJSnodejsChrome (V8)Debian/Ubuntu alias of Node
JavaScriptCorejscSafari (WebKit)macOS only (bundled)
SpiderMonkeyjsFirefox (Gecko)js package on most Linux distros
PhantomjsphantomjsWebKitupstream abandoned 2018
SlimerJSslimerjsGeckoupstream abandoned 2021
NashornjjsJava (JVM)Oracle JDK; removed since JDK 15
JScriptcscript //E:jscript //NologoIE (Trident)Windows only (built-in); uses temp file

ExeJS auto-detects the first available runtime in the order above. Use reset_runtime to pick one explicitly.

API Reference

Top-level functions

exejs.evaluate(source: str, timeout: float | None = None) -> Any
exejs.execute(source: str, timeout: float | None = None) -> Any
exejs.compile(source: str = '', cwd: str | None = None) -> RuntimeCompileContext

async exejs.evaluate_async(source: str, timeout: float | None = None) -> Any
async exejs.execute_async(source: str, timeout: float | None = None) -> Any

Compile context

ctx = exejs.compile(source='', cwd=None)

ctx.call(key: str, *args, timeout: float | None = None) -> Any        # key supports paths like 'calc.mul'
ctx.evaluate(source: str, timeout: float | None = None) -> Any
ctx.execute(source: str, timeout: float | None = None) -> Any

async ctx.call_async(key: str, *args, timeout: float | None = None) -> Any
async ctx.evaluate_async(source: str, timeout: float | None = None) -> Any
async ctx.execute_async(source: str, timeout: float | None = None) -> Any

Runtime management

exejs.reset_runtime(name: str) -> None                       # switch runtime by name
exejs.get_current_runtime() -> Runtime
exejs.get_current_runtime_name() -> str
exejs.find_available_runtime() -> Runtime                    # first available
exejs.find_all_runtime_name_list(is_available: bool = True) -> list[str]

Exception hierarchy

ExejsError
├── ExejsRuntimeNameError          # unknown runtime name passed to reset_runtime
├── ExejsRuntimeUnavailableError   # no runtime found / runtime not on PATH
├── ExejsProcessExitError          # subprocess failed / non-zero exit
├── ExejsProgramError              # JS crashed or returned invalid JSON
└── ExejsTimeoutError              # timeout exceeded

Advanced

Compile & call (context reuse)

Reuse a single JS context across multiple calls instead of re-launching a runtime each time:

import exejs

ctx = exejs.compile('function add(x, y) { return x + y; }')
ctx.call('add', 1, 2)  # 3
ctx.call('add', 10, 20)  # 30

Structured arguments

call() auto JSON-serializes arguments and supports property paths as the key:

# call an object method (key may be a property path)
ctx = exejs.compile('var calc = { mul: function(a, b) { return a * b; } };')
ctx.call('calc.mul', 3, 4)  # 12

# call with a dict argument (auto JSON-serialized)
ctx = exejs.compile('function greet(u) { return "hi " + u.name + ", age " + u.age; }')
ctx.call('greet', {'name': 'Tom', 'age': 18})  # 'hi Tom, age 18'

Async API

Async is useful inside async web frameworks (FastAPI, aiohttp) or when you want to avoid blocking the event loop:

import asyncio
import exejs

asyncio.run(exejs.evaluate_async("'red yellow blue'.split(' ')"))

Timeout

timeout (seconds) applies to both sync and async variants. On expiry the subprocess is killed and ExejsTimeoutError is raised:

try:
    exejs.execute('while (true) {}', timeout=2.0)
except exejs.ExejsTimeoutError as e:
    print('killed:', e)

Choosing a runtime

import exejs

# see what's available on this machine
print(exejs.find_all_runtime_name_list())  # e.g. ['Node', 'JScript']

# force a specific one
exejs.reset_runtime('Node')
print(exejs.get_current_runtime_name())  # 'Node'

Working directory

compile(source, cwd=...) sets the subprocess working directory, useful when your JS reads relative files:

ctx = exejs.compile('return require("./config.json")', cwd='/path/to/project')
ctx.evaluate('')

FAQ

  • ExejsRuntimeUnavailableError: Unable to find available javascript runtime — No JS runtime is installed or none is on PATH. Install Node.js from nodejs.org and reopen your terminal, or call exejs.reset_runtime('JScript') on Windows where cscript is built in.
  • Antivirus blocks execution — If you are using the JScript runtime (Windows), it still writes a temp file which may trigger antivirus. Switch to Node (exejs.reset_runtime('Node')) to avoid this. All other runtimes use stdin and should not be affected.
  • cscript not recognized on Windows — JScript is provided by Windows Script Host. Ensure cscript.exe exists (usually at C:\Windows\System32\) and is on PATH; on stripped-down Windows images you may need to enable the Windows Script Host feature.
  • Blocks the event loop in asyncio code — The sync evaluate / execute block the calling thread. Inside an async framework use evaluate_async / execute_async instead so the event loop stays responsive.

Changelog

See change_log.md.

Contributing

Pull requests are welcome. To set up a dev environment:

git clone https://github.com/UlionTse/exejs.git
cd exejs
pip install -e .
pip install pytest
pytest exejs/tests

Run pytest exejs/tests before submitting a PR. Code style follows PEP 8. Please open an issue first to discuss any non-trivial change.

Acknowledgements

Notable Usage

ExeJS is a core dependency of Translators, where it handles the JavaScript execution layer that translation engines rely on.

License

Apache-2.0 © UlionTse

javascript

Project health

PyPI

Latest release

1.0.1 · Jun 28, 2026

Releases this year

3