mini-racer 0.14.1
Minimal, modern embedded V8 for Python.
Requires Python: >=3.10
Keywords: py_mini_racer

[![PyPI status indicator](https://img.shields.io/pypi/v/mini_racer.svg)](https://pypi.python.org/pypi/mini_racer)
[![Github workflow status indicator](https://github.com/bpcreech/PyMiniRacer/actions/workflows/build.yml/badge.svg)](https://github.com/bpcreech/PyMiniRacer/actions/workflows/build.yml)
[![ISC License](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)

Minimal, modern embedded V8 for Python.

![MiniRacer logo: a V8 with a very snakey 8](https://github.com/bpcreech/PyMiniRacer/raw/main/py_mini_racer.png)

[Full documentation](https://bpcreech.com/PyMiniRacer/).

## In brief

- Latest ECMAScript support
- Web Assembly support
- Unicode support
- Thread safe
- Re-usable contexts

MiniRacer can be easily used by Django or Flask projects to minify assets, run babel or
WASM modules.

PyMiniRacer was created by [Sqreen](https://github.com/sqreen), and originally lived at
<https://github.com/sqreen/PyMiniRacer> with the PyPI package
[`py-mini-racer`](https://pypi.org/project/py-mini-racer/). After dicussion with the
original Sqreen team, [I](https://bpcreech.com) have created a new official home for at
<https://github.com/bpcreech/PyMiniRacer> with a new PyPI package
[`mini-racer`](https://pypi.org/project/mini-racer/) (_note: no `py-`_). See
[the full history](https://bpcreech.com/PyMiniRacer/history) for more.

## Examples

MiniRacer is straightforward to use:

```sh
    $ pip install mini-racer
```

and then:

```python
    $ python3
    >>> from py_mini_racer import MiniRacer
    >>> ctx = MiniRacer()
    >>> ctx.eval("1+1")
    2
    >>> ctx.eval("var x = {company: 'Sqreen'}; x.company")
    'Sqreen'
    >>> print(ctx.eval("'❤'"))
    ❤
    >>> ctx.eval("var fun = () => ({ foo: 1 });")
```

Variables are kept inside of a context:

```python
    >>> ctx.eval("x.company")
    'Sqreen'
```

You can evaluate whole scripts within JavaScript, or define and return JavaScript
function objects and call them from Python (_new in v0.11.0_):

```python
    >>> square = ctx.eval("a => a*a")
    >>> square(4)
    16
```

JavaScript Objects and Arrays are modeled in Python as dictionaries and lists (or, more
precisely,
[`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)
and
[`MutableSequence`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableSequence)
instances), respectively (_new in v0.11.0_):

```python
    >>> obj = ctx.eval("var obj = {'foo': 'bar'}; obj")
    >>> obj["foo"]
    'bar'
    >>> list(obj.keys())
    ['foo']
    >>> arr = ctx.eval("var arr = ['a', 'b']; arr")
    >>> arr[1]
    'b'
    >>> 'a' in arr
    True
    >>> arr.append(obj)
    >>> ctx.eval("JSON.stringify(arr)")
    '["a","b",{"foo":"bar"}]'
```

Meanwhile, `call` uses JSON to transfer data between JavaScript and Python, and converts
data in bulk:

```python
    >>> ctx.call("fun")
    {'foo': 1}
```

Composite values are serialized using JSON. Use a custom JSON encoder when sending
non-JSON encodable parameters:

```python
    import json

    from datetime import datetime

    class CustomEncoder(json.JSONEncoder):

            def default(self, obj):
                if isinstance(obj, datetime):
                    return obj.isoformat()

                return json.JSONEncoder.default(self, obj)
```

```python
    >>> ctx.eval("var f = function(args) { return args; }")
    >>> ctx.call("f", datetime.now(), encoder=CustomEncoder)
    '2017-03-31T16:51:02.474118'
```

MiniRacer is ES6 capable:

```python
    >>> ctx.execute("[1,2,3].includes(5)")
    False
```

JavaScript `null` and `undefined` are modeled in Python as `None` and `JSUndefined`,
respectively:

```python
    >>> list(ctx.eval("[null, undefined]"))
    [None, JSUndefined]
```

You can prevent runaway execution in synchronous code using the `timeout_sec` parameter:

```python
    >>> ctx.eval('while (true) {}', timeout_sec=2)
    # Spins for 2 seconds and then emits a traceback ending with...
        raise JSTimeoutException from e
    py_mini_racer._exc.JSTimeoutException: JavaScript was terminated by timeout
    >>> func = ctx.eval('() => {while (true) {}}')
    >>> func(timeout_sec=2)
    # Spins for 2 seconds and then emits a traceback ending with...
        raise JSTimeoutException from e
    py_mini_racer._exc.JSTimeoutException: JavaScript was terminated by timeout
```

MiniRacer supports asynchronous execution using JS `Promise` instances (_new in
v0.10.0_):

```python
    >>> promise = ctx.eval(
    ...     "new Promise((res, rej) => setTimeout(() => res(42), 10000))")
    >>> promise.get()  # blocks for 10 seconds, and then:
    42
```

For more deterministic cleanup behavior, we strongly recommend allocating a MiniRacer
from a context manager (_new in v0.14.0_):

```python
    >>> from py_mini_racer import mini_racer
    >>> with mini_racer() as ctx:
    ...     print(ctx.eval("Array.from('foobar').reverse().join('')"))
    raboof
```

MiniRacer uses `asyncio` internally to manage V8. Both `MiniRacer()` and the
`mini_racer()` context manager will capture the currently-running event loop, or you can
specify a loop explicitly, and in non-async contexts, `MiniRacer` will launch its own
event loop with its own background thread to service it. (_new in v0.14.0_)

```python
    >>> from py_mini_racer import MiniRacer, mini_racer
    >>> ctx = MiniRacer()  # launches a new event loop in a new thread
    >>> with mini_racer() as ctx:  # same: launches a new event loop in a new thread
    ...     pass
    ...
    >>> async def demo():
    ...     with mini_racer() as ctx:  # reuses the running event loop
    ...         pass
    ...
    >>> import asyncio
    >>> asyncio.run(demo())
    >>> my_loop = asyncio.new_event_loop()
    >>> with mini_racer(my_loop) as ctx:  # uses the specified event loop
    ...     pass
```

When calling into MiniRacer from async code, you must await promises using `await`
(instead of `promise.get()`):

```python
    % python -m asyncio
    >>> from py_mini_racer import mini_racer
    >>> with mini_racer() as ctx:
    ...     promise = ctx.eval(
    ...         "new Promise((res, rej) => setTimeout(() => res(42), 10000))")
    ...     print(await promise)  # yields for 10 seconds, and then:
    ...
    42
```

`MiniRacer` does not support the `timeout_sec` parameter in async evaluation. Instead
request a cancelable evaluation and use a construct like `asyncio.wait_for`:

```python
    % python -m asyncio
    >>> from py_mini_racer import mini_racer
    >>> with mini_racer() as ctx:
    ...     # Use eval_cancelable(...), which has async semantics:
    ...     await asyncio.wait_for(ctx.eval_cancelable('while (true) {}'), timeout=2)
    # Spins for 2 seconds and then emits a traceback ending with...
        raise TimeoutError from exc_val
    TimeoutError
    >>> with mini_racer() as ctx:
    ...     func = ctx.eval('() => {while (true) {}}')
    ...     # Upgrade func using .cancelable(), which introduces async semantics:
    ...     cancelable_func = func.cancelable()
    ...     await asyncio.wait_for(cancelable_func(), timeout=2)
    # Spins for 2 seconds and then emits a traceback ending with...
        raise TimeoutError from exc_val
    TimeoutError
```

You can install callbacks from JavaScript to Python (_new in v0.12.0_). Only async
callbacks are supported:

```python
    % python -m asyncio
    >>> from py_mini_racer import mini_racer
    >>> async def read_file(fn):
    ...     with open(fn) as f:  # (or aiofiles would be even better here)
    ...         return f.read()
    ...
    >>> with mini_racer() as ctx:
    ...     async with ctx.wrap_py_function(read_file) as jsfunc:
    ...         # "Install" our (async) JS function on the global "this" object:
    ...         ctx.eval('this')['read_file'] = jsfunc
    ...         d = await ctx.eval('read_file("/usr/share/dict/words")')
    ...         print(d.split()[0:10])
    ['A', 'AA', 'AAA', "AA's", 'AB', 'ABC', "ABC's", 'ABCs', 'ABM', "ABM's"]
```

_Note that adding Python callbacks may degrade the security properties of PyMiniRacer!
See [PyMiniRacer's security goals](ARCHITECTURE.md#security-goals)._

MiniRacer supports [the ECMA `Intl` API](https://tc39.es/ecma402/):

```python
    # Indonesian dates!
    >>> ctx.eval('Intl.DateTimeFormat(["ban", "id"]).format(new Date())')
    '16/3/2024'
```

V8 heap information can be retrieved:

```python
    >>> ctx.heap_stats()
    {'total_physical_size': 1613896,
     'used_heap_size': 1512520,
     'total_heap_size': 3997696,
     'total_heap_size_executable': 3145728,
     'heap_size_limit': 1501560832}
```

A WASM example is available in the
[`tests`](https://github.com/bpcreech/PyMiniRacer/blob/master/tests/test_wasm.py).

## Compatibility

PyMiniRacer is compatible with Python 3.10-3.14 and is based on `ctypes`.

PyMiniRacer is distributed using [wheels](https://pythonwheels.com/) on
[PyPI](https://pypi.org/). The wheels are intended to provide compatibility with:

| OS                              | x86_64 | aarch64 |
| ------------------------------- | ------ | ------- |
| macOS ≥ 10.9                    | ✓      | ✓       |
| Windows ≥ 10                    | ✓      | ✓       |
| Ubuntu ≥ 20.04                  | ✓      | ✓       |
| Debian ≥ 11                     | ✓      | ✓       |
| RHEL ≥ 9                        | ✓      | ✓       |
| other Linuxes with glibc ≥ 2.27 | ✓      | ✓       |
| Alpine ≥ 3.19                   | ✓      | ✓       |
| other Linux with musl ≥ 1.2     | ✓      | ✓       |

In order to run on Alpine you must install `gcompat` and run with
`LD_PRELOAD="/lib/libgcompat.so.0"`.

If you have a up-to-date pip and it doesn't use a wheel, you might have an environment
for which no wheel is built. Please open an issue.

## Developing and releasing PyMiniRacer

See [the contribution guide](CONTRIBUTING.md).

## Credits

Built with love by [Sqreen](https://www.sqreen.com).

PyMiniRacer launch was described in
[`this blog post`](https://web.archive.org/web/20230526172627/https://blog.sqreen.com/embedding-javascript-into-python/).

PyMiniRacer is inspired by [mini_racer](https://github.com/SamSaffron/mini_racer), built
for the Ruby world by Sam Saffron.

In 2024, PyMiniRacer was revived, and adopted by [Ben Creech](https://bpcreech.com).
Upon discussion with the original Sqreen authors, we decided to re-launch PyMiniRacer as
a fork under <https://github.com/bpcreech/PyMiniRacer> and
<https://pypi.org/project/mini-racer/>.


## Release history


### 0.14.1 (2026-01-31)

- Fix memory leak when tracking JS objects in Python. We were never calling
  v8::Persistent::Reset(). Now we use v8::Global which has no such requirement.
- In the C++ implementation, rename BinaryValue to just Value and optimize things a bit
  by collapsing various data members into a Variant.
- Upgrade to V8 14.4 from 14.3.

### 0.14.0 (2026-01-03)

- Major revamp of Python-side async handling: `PyMiniRacer` now manages most
  asynchronous work (in particular, _cancelable_ work) through an `asyncio` event loop.
  This is intended to make `PyMiniRacer` easier to reason about, removing opportunities
  for deadlocks and race conditions.
- We expose new async methods for function evaluation which unblock the event loop for
  long calculations while honoring standard `asyncio` task cancellation semantics.
- To improve determinism during teardown, we expose and strongly recommend a new context
  manager, `mini_racer` to more explicitly create and tear down `MiniRacer` instances.

### 0.13.2 (2025-12-25)

- Improve performance of function calls by exposing Array.prototype.push (avoiding two
  round trips to get array size and then slice it, for function call argv construction).
- Various internal simplifications intended to improve maintainability. None of these
  should be externally visible.

### 0.13.1 (2025-12-24)

- Fix MacOS wheels to not require MacOS 15.

### 0.13.0 (2025-12-07)

- Upgrade to V8 14.3 from 12.6.
- Add Windows Arm support!
- Alpine support is now reduced to using `gcompat` and
  `LD_PRELOAD="/lib/libgcompat.so.0"`.
- Crank up strictness of ruff and mypy, fixing uncovered typing issues.
- Internally: Switch from Hatch and pre-commit to uv, Justfile, and Prettier.
- Internally: Cross compile for arm (testing on the new native Github runners!) instead
  of using emulation + sccache. This cuts the build time about 10x!

### 0.12.4 (2024-06-16)

- Upgrade to V8 12.6 from 12.4.

### 0.12.3 (2024-05-25)

- Fix potential hang if JavaScript calls a function produced by `wrap_py_function` while
  we're tearing it down.

### 0.12.2 (2024-05-20)

- Add optional context manager and `.close()` semantics to Python `MiniRacer` class.

- Fixed a potential hang on MiniRacer teardown if MiniRacer is executing a microtask
  which loops infinitely.

- Switch C++ side of MiniRacer to a more straightforward object lifecycle management
  model.

### 0.12.1 (2024-05-18)

- Update to V8 12.4. This includes fixes for CVE-2024-3159, CVE-2024-3156, and
  CVE-2024-2625. These vulnerabilities in V8 would impact PyMiniRacer users who are
  running untrusted and adversarial JavaScript code.

### 0.12.0 (2024-04-29)

- Added support for installing callbacks from JS back into Python, using
  MiniRacer.wrap_py_function.

- Refactored the Python implementation into many internal files. This should mostly not
  present a breaking change, except for code which reaches into internal (`_`-prefixed)
  variables.

### 0.11.1 (2024-04-08)

- Fixed Python crash on long-running microtasks, introduced in v0.8.1 (before which
  long-running microtasks would probably not run at all).

- Fixed memory leak on the exception object reported when an `eval` times out.

- Hardened the memory management of JS value interchange, context tracking, and
  asynchronous task tracking between C++ and Python.

- Added exhaustive typing (now with a MyPy pre-commit to verify!)

- Added a test asserting that [the v8 sandbox](https://v8.dev/blog/sandbox) is enabled
  on all platforms we build wheels for.

### 0.11.0 (2024-04-03)

- Added a `MutableMapping` (`dict`-like) interface for all derivatives of JS Objects,
  and a `MutableSequence` (`list`-like) interface for JS Arrays. You can now use
  Pythonic idioms to read and write Object properties and Array elements in Python,
  including recursively (i.e., you can read Objects embedded in other objects, and embed
  your own).

- Added ability to directly call `JSFunction` objects from Python. E.g.,
  `mr.eval("a => a*a")(4)` parses the given number-squaring code into a function,
  returns a handle to that function to Python, calls it with the number `4`, and
  recieves the result of `16`.

- Added a `JSUndefined` Python object to model JavaScript `undefined`. This is needed to
  properly implement the above interface for reading Object and Array elements.
  _Unfortunately, this may present a breaking change for users who assume JavaScript
  `undefined` is modeled as Python `None`._

- Removed an old optimization for `eval` on simple no-argument function calls (i.e.,
  `myfunc()`). The optimization only delivered about a 17% speedup on no-op calls (and
  helped relatively _less_ on calls which actually did work), and for the purpose of
  optimizing repeated calls to the same function, it's now redundant with extracting and
  calling the function from Python, e.g., `mr.eval("myfunc")()`.

- Hardening (meaning "fixing potential but not-yet-seen bugs") related to freeing
  `Value` instances (which convey data from C++ to Python).

- More hardening related to race conditions on teardown of the `MiniRacer` object in the
  unlikely condition that `eval` operations are still executing on the C++ side, and
  abandoned on the Python side, when Python attempts to garbage collect the `MiniRacer`
  object.

### 0.10.0 (2024-03-31)

- Updated to V8 12.3 from V8 12.2 now that Chromium stable is on 12.3.

- Added Python-side support for JS Promises. You can now return a JS Promise from code
  executed by `MiniRacer.eval`, and PyMiniRacer will convert it to a Python object which
  has a blocking `promise.get()` method, and also supports `await promise` in `async`
  Python functions.

- Added a `setTimeout` and `clearTimeout`. These common functions live in the Web API
  standard, not the ECMAScript standard, and thus don't come with V8, but they're so
  ubiquitious we now ship an implemention with `PyMiniRacer`.

### 0.9.0 (2024-03-30)

- Revamped JS execution model to be out-of-thread. Python/C++ interaction now happens
  via callbacks.

- Consequently, Control+C (`KeyboardInterrupt`) now interrupts JS execution.

- Hardened C++-side thread safety model, resolving potential race conditions introduced
  in v0.8.1 (but not actually reported as happening anywhere).

- Further improved JS exception reporting; exception reports now show the offending code
  where possible.

- Introduced `timeout_sec` parameter to `eval`, `call`, and `execute` to replace the
  `timeout`, which unfortunately uses milliseconds (unlike the Python standard library).
  In the future we may emit deprecation warnings for use of `timeout`.

### 0.8.1 (2024-03-23)

- A series of C++ changes which should not impact the behavior of PyMiniRacer:
- Refactoring how we use V8 by inverting the control flow. Before we had function
  evaluations which ran and drained the message loop. Now we have an always-running
  message loop into which we inject function evaluations. This seems to be the preferred
  way to use V8. This is not expected to cause any behavior changes (but, in tests,
  makes
  [microtask competion](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide)
  more consistent).
- Refactoring the C++ implementation into multiple components to make startup and
  teardown logic more robust.
- Added tests for the existing fast-function-call path.
- Also, simplified Python conversion of C++ evaluation results.

### 0.8.0 (2024-03-18)

- General overhaul of C++ implementation to better adhere to modern best practice. This
  should have no visible impact except for the following notes...
- Exposed the hard memory limit as a context-specific (as opposed to `eval`-specific)
  limit, since that's how it worked all along anyway. The `max_memory` `eval` argument
  still works for backwards compatibility purposes.
- Correct message type of some exceptions to `str` instead of `bytes` (they should all
  be `str` now).
- Added better messages for JS parse errors.
- Added backtraces for more JS errors.
- Added some really basic Python typing.

### 0.7.0 (2024-03-06)

- Update V8 to 12.2
- Drop Python 2 support
- Fix small Python 3.12 issue and add testing for Python 3.9-3.12
- Add aarch64 support for Mac and Linux
- Revamp DLL loading to be compliant with Python 3.9-style resource loading. This may
  present a small breaking change for advanced usage; the `EXTENSION_PATH` and
  `EXTENSION_NAME` module variables, and `MiniRacer.v8_flags` and `MiniRacer.ext` class
  variable have all been removed.
- Add support for the [ECMAScript internalization API](https://v8.dev/docs/i18n) and
  thus [the ECMA `Intl` API](https://tc39.es/ecma402/)
- Use [fast startup snapshots](https://v8.dev/blog/custom-startup-snapshots)
- Switch from setuptools to Hatch
- Switch from tox to Hatch
- Switch from flake8 and isort to Hatch's wrapper of Ruff
- Switch from Sphinx to mkdocs (and hatch-mkdocs)
- Switch from unittest to pytest
- Add ARCHITECTURE.md and lots of code comments

### 0.6.0 (2020-04-20)

- Update V8 to 8.9
- Optimize function calls without arguments
- Switch V8 to single threaded mode to avoid crashes after fork
- Switch to strict mode by default
- Revamp documentation

### 0.5.0 (2020-02-25)

- Update V8 to 8.8

### 0.4.0 (2020-09-22)

- Universal wheels for Linux, Mac and Windows
- Fallback to source package for Alpine Linux

### 0.3.0 (2020-06-29)

- Introduce a strict mode
- Fix array conversion when size changes dynamically (CVE-2020-25489)

### 0.2.0 (2020-03-11)

- Support for Alpine Linux
- Avoid pip private modules in setup.py

### 0.2.0b1 (2020-01-09)

- Support for Windows 64 bits
- Support for Python 3.8
- Upgrade V8 to 7.8
- Support soft memory limits

### 0.1.18 (2019-01-04)

- Support memory and time limits

### 0.1.17 (2018-19-12)

- Upgrade libv8
- Fix a memory leak

### 0.1.16 (2018-07-11)

- Add wheel for Python without PyMalloc

### 0.1.15 (2018-06-18)

- Add wheel for Python 3.7

### 0.1.14 (2018-05-25)

- Add support for pip 10
- Update package metadata

### 0.1.13 (2018-03-15)

- Add heap_stats function
- Fix issue with returned strings containing null bytes

### 0.1.12 (2018-17-04)

- Remove dependency to enum

### 0.1.11 (2017-07-11)

- Add compatibility for centos6

### 0.1.10 (2017-03-31)

- Add the possibility to pass a custom JSON encoder in call.

### 0.1.9 (2017-03-24)

- Fix the compilation for Ubuntu 12.04 and glibc < 2.17.

### 0.1.8 (2017-03-02)

- Update targets build for better compatibility with old Mac OS X and linux platforms.

### 0.1.7 (2016-10-04)

- Improve general performances of the JS execution.
- Add the possibility to build a different version of V8 (for example with debug
  symbols).
- Fix a conflict that could happens between statically linked libraries and dynamic
  ones.

### 0.1.6 (2016-08-12)

- Add error message when py_mini_racer sdist fails to build asking to update pip in
  order to download the pre-compiled wheel instead of the source distribution.

### 0.1.5 (2016-08-04)

- Build py_mini_racer against a static Python. When built against a shared library
  python, it doesn't work with a static Python.

### 0.1.4 (2016-08-04)

- Ensure JSEvalException message is converted to unicode

### 0.1.3 (2016-08-04)

- Fix extension loading for python3
- Add a make target for building distributions (sdist + wheels)
- Fix eval conversion for python 3

### 0.1.2 (2016-08-03)

- Fix date support
- Fix Dockerfile for generating python3 wheels

### 0.1.1 (2016-08-02)

- Fix sdist distribution.

### 0.1.0 (2016-08-01)

- First release on PyPI.