Skip to main content

concurrent-c-node

Call Node (and npm packages) from Python. Native types, exceptions, callbacks, and async all cross the boundary.

Why use this

Call real Node from Python — require, native addons, npm — not a JS engine in-process. One child, shm for bulk, from cc_node import require in Jupyter/Colab with no %load_ext. pythonia is the packaged peer; this is faster on the wire and honest about bulk and callbacks.

Part of Concurrent-C — a strict C11-superset preprocessor: .ccs lowers to plain C and compiles with your host C compiler. (This bridge itself is pure Python stdlib — no native build.)

Map of the three boundaries (CC hosts JS, native modules, this package bridge): JS / Python interop.

from cc_node import require          # one session child (lazy)
_ = require('lodash')                # cwd node_modules
_.chunk([1, 2, 3, 4, 5], 2)          # [[1, 2], [3, 4], [5]]

A private child (create()) is still there when you want N Nodes or an explicit lifetime — not required for the first call.

import cc_node
js = cc_node.create()                # a private Node, not the session
semver = js.require('semver')
semver.satisfies('1.2.3', '^1.0.0')  # True
js.destroy()                         # or: with cc_node.create() as js:

The other direction (Python from Node): concurrent-c-python (in-process by default; vs pymport / ncp / pythonia in that README).

Every create() is a separate Node — real addons, crash isolation, measurable wire. N domains = N processes. require() / get() share one session for the process (the Jupyter kernel).

this package CC hosted (cc_js_new(false, …))
API require() / create() .ccs program
Where child node libnode in-process
Hot call ~105µs RTT sub-µs (needs libnode)
Bulk shm (~9.5ms / 8MB) in-process
Parallelism N children one process
Crash child dies; parent lives shared fate

Cheat sheet

  • Always a child node. A call blocks until JS answers; thenables wait in the child. No { async: true }.
  • from cc_node import require is the session. create() is a private child. reset() / %js_reset / %reset / atexit tear the session down.
  • Scalars / None materialize; empty {} stays a handle; everything else is a JsHandle until str() / attrs / a call.
  • Notebook: import cc_node registers %%js (no %load_ext). Same session as require().
  • eval() is one RTT, no extra globals. %%js / eval_cell install cwd require once.
  • --bind is Object.assign(globalThis, …) of names you name (wire types only; no pickle). Missing names refuse.
  • First Ctrl-C finishes the in-flight call (wire stays in sync). Interrupt again to kill the child.
pip install concurrent-c-node                 # needs node on PATH
pip install 'concurrent-c-node[jupyter]'      # IPython (%%js); require() does not need this
python -m cc_node.examples.use_node
python -m cc_node.examples.bench_wire
python -m cc_node.benchmarks.multi_domain
python -m cc_node.benchmarks.vs_alts          # vs pythonia / DIY node / pythonmonkey / mini-racer

Jupyter / Colab

Same verb as pythonia: require. No %load_ext, no create(), no destroy() for the happy path. One session child for the kernel; console.log lands in the cell.

%pip install concurrent-c-node
# if `node` is missing (typical Colab):
!apt-get install -y nodejs

from cc_node import require
require('lodash').chunk([1, 2, 3, 4, 5], 2)

import cc_node also registers %%js (IPython already running; the [jupyter] extra is only if you want magics without IPython already installed).

%%js
console.log('hi')              # shows in the cell
require('lodash').chunk([1, 2, 3, 4, 5], 2)
xs = [1, 2, 3, 4]

%%js -b xs -t chunks
xs.map(x => x * 2)             # wire types only; no pickle fallback
from cc_node import require session require; spawns on first call
import cc_node registers magics; does not spawn until first require() / %%js
%load_ext cc_node same, idempotent; not required
%js 1+1 / %%js eval on the same session; last expression is the result
-b xs / --bind xs,n publish those Python names on globalThis for the cell
-t chunks / --to store the result in the notebook namespace
%js_stats handle-table size (spawns if needed)
%js_reset / cc_node.reset() destroy() the session child; %reset does this too
cc_node.get() the session the magics use; create() is still a private child
cc_node.kernel() alias of get()
cc_node.require('fs') get().require('fs')
JsHandle display cheap JsHandle #3 — repr does not cross the wire

eval() is unchanged (one RTT, no extra globals). %%js / eval_cell install cwd require once so cells look like Node (require('path')). --bind is Object.assign(globalThis, …) — missing names and non-wire types (DataFrame, a set) fail articulately. Reserved names (require, process, globalThis, …) refuse so a bind cannot shadow Node.

Interrupt. First Ctrl-C does not abandon the in-flight reply (that would desync the wire and drop callbacks). The call finishes, the result is discarded, the domain stays up. Interrupt again to kill the child — cooperative destroy() cannot stop a JS for (;;) {}. Same honesty as the rest of the bridge.

JS kernels (tslab, Deno Jupyter) are concurrent-c-python; Colab is not that. There, default create() blocks the kernel thread — py.task / { isolated: true } when the loop must stay live.

Measured

bench_wire · cc_node_bridge_py_20260810.txt:

what result
spawn (first eval) 28ms
wire RTT 105µs
Python callback round trip 153µs
8MB array('d') via shm 9.5ms
same 8MB as JSON list 499ms (~52×)

Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private 0700 dir, 0600 files, removed with the bridge.

Vs pythonia / pythonmonkey / mini-racer / DIY node

Most “JS from Python” libraries are not Node. pythonia (PyPI javascript, JSPyBridge) is the packaged peer that is: require() on import, one child. Bulk is a sum over 1M floats (.length on an in-process wrapper is free and lies). Snapshot: cc_node_vs_alts_20260813.txt · harness: benchmarks/vs_alts.py.

cc-node pythonia DIY JSON stdio node -e each pythonmonkey mini-racer
identity RTT 20µs 42µs 19µs 23ms <1µs 104µs
callback 40µs 106µs 1µs
8MB typed / list 7.5ms shm / 266ms — / 481ms — / 155ms — / 609ms — / 78ms
require('fs') yes yes yes yes no no
process child node child node child node new process/call SpiderMonkey in-process V8 isolate

Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node (require('fs'), native addons, callbacks, stdout stays yours): this package — ~2× pythonia on identity, shm for bulk, Python callables are sync (pythonia’s JS side sees a Promise). Isolated node -e per call is ~1000× a persistent child. Optional engines SKIP if not importable — not package deps.

The other direction (Python from Node): concurrent-c-python vs pymport / ncp / pythonia.

Surface

  • Plain data (numbers, str, bool, None, lists, non-empty dicts) by value; else a domain-owned handle (attrs, calls, str()String()). Non-finite floats are tagged.
  • Handles are per-domain. stats() / release() / idempotent destroy(); afterwards: bridge is closed.
  • eval_cell(src, bindings=) is the notebook door (%%js): cwd require once, optional globalThis binds; eval() stays one RTT. get() / require() / eval() at module level share that session.
  • Crash isolation, not a sandbox. destroy() is cooperative; an in-flight CPU-bound call finishes or you kill the child (bridge_stress.md).

Promises

Awaited in the child before the reply — no async/await on the Python side. Same honesty as concurrent-c-python: a call blocks until the other runtime answers.

fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
fetchish(21)   # {'doubled': 42}

Callbacks

mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
# map passes (value, index, array) — take *rest

Exceptions cross both ways with messages intact.

Buffers

bytes / array.array / 1-D numpy → typed arrays (and back). Small inline; large via shm.

import array
total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
total(array.array('d', range(1_000_000)))

Common issues

Cannot find module. require / import resolve from the Python process cwd (node_modules next to your notebook or program), not from this wheel’s site-packages, and this package does not npm install on a miss (pythonia does). npm install lodash in that directory is the fix; or create(node=…) / CC_NODE_BIN when the wrong Node is on PATH.

Empty {} stays a handle

An empty object has to stay on the Node side — a materialized Python dict would lose later property use that matches Node. So js.eval('({})') returns a live handle:

o = js.eval('({})')                    # JsHandle, not {}
js.eval('(o) => { o.x = 1; return o.x }')(o)   # 1
js.eval('({a: 1})')                    # {'a': 1} — data return

Non-empty plain objects still cross as Python dicts. Same-domain handles chain (h.update(…).digest(…)).

Wire cost vs tiny work. Round trip is ~100µs; a one-line JS helper on three numbers loses to pure Python. Prefer Python (or a native CC module) for small/hot work; use the bridge when Node/npm owns the kernel. Multi-core: python -m cc_node.benchmarks.multi_domain (~2.8× on 3 domains here).

Choosing node

  1. create(node='/path/to/node')
  2. CC_NODE_BIN
  3. node on PATH

Packages: cwd node_modules, same as Node itself.

From Concurrent-C (not Python): cc_js_new(false, &a) hosted (libnode), or cc_js_new(true, &a) for this wire — jsdemo.shcc.

Publishing

PyPI Trusted Publishing (OIDC) — no API token:

  1. Publishing settings: owner sreekotay, repo concurrent-c, workflow publish-cc-node.yml, environment pypi
  2. GitHub Environment pypi
  3. Bump pyproject.toml, push, then:
gh workflow run publish-cc-node.yml

Both bridges (npm OIDC + PyPI OIDC):

./scripts/publish_bridges.sh --publish --minor
# fallbacks: --npm-local / --pypi-twine

Examples: use_node, bench_wire, benchmarks.multi_domain, benchmarks.vs_alts. Jupyter: from cc_node import require (or import cc_node then %%js). Stress: stress/bridge/.
Own hot path in C/CC → native module (40–90ns) — JS / Python interop.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

concurrent_c_node-0.23.2.tar.gz (28.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

concurrent_c_node-0.23.2-py3-none-any.whl (26.8 kB view details)

Uploaded Python 3

File details

Details for the file concurrent_c_node-0.23.2.tar.gz.

File metadata

  • Download URL: concurrent_c_node-0.23.2.tar.gz
  • Upload date:
  • Size: 28.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for concurrent_c_node-0.23.2.tar.gz
Algorithm Hash digest
SHA256 4c4b1b39342b0414b96a066b02c863fdc53b487d7dba352d1675a664ca8578c8
MD5 b9258ad7c869739833c15d2a4d20fe00
BLAKE2b-256 987383ad16a3a7ae191d4161ab038bbcef8c1b2a4a82b7f461c233227c9fbdb7

See more details on using hashes here.

Provenance

The following attestation bundles were made for concurrent_c_node-0.23.2.tar.gz:

Publisher: publish-cc-node.yml on sreekotay/concurrent-c

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file concurrent_c_node-0.23.2-py3-none-any.whl.

File metadata

File hashes

Hashes for concurrent_c_node-0.23.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9d476d05c4a603f71555b2e097d82cf72bf549eb0e2e2e5fec0b6afd11752dd3
MD5 b862160bbdd10fa734b9640e48611b87
BLAKE2b-256 68bf18770b1b47f9a74e6360bce9a307a636e39848e92fb923b5bc800b25d9e0

See more details on using hashes here.

Provenance

The following attestation bundles were made for concurrent_c_node-0.23.2-py3-none-any.whl:

Publisher: publish-cc-node.yml on sreekotay/concurrent-c

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.23.3

2 files

This release

0.23.2 This release

2 files

0.23.1

2 files

0.23.0

2 files

0.22.1

2 files

0.22.0

2 files

0.21.0

2 files

0.20.0

2 files

0.19.0

2 files

0.18.0

2 files

0.17.11

2 files

0.17.10

2 files

0.17.9

2 files

0.17.8

2 files

0.17.7

2 files

0.17.6

2 files

0.17.5

2 files

0.17.4

2 files

0.17.3

2 files

0.17.2

2 files

0.17.1

2 files

0.17.0

2 files

0.16.0

2 files

0.15.0

2 files

0.14.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page