# Build a polyglot app

> Combine an npm package and a PyPI package in one Elide program: pass values, functions and errors between JavaScript and Python with --interop.

This page builds one program that uses an npm package from JavaScript and a PyPI package from Python, in one process.

| Needs | How |
| --- | --- |
| Python | `elide setup python`; `elide info` lists `python` under `Languages` |
| Cross-language calls | `--interop` on every run; there is no manifest setting |
| npm packages | `dependencies.npm` in `elide.pkl`, then `elide install --ecosystems=npm` |
| PyPI packages | A Python 3.13 virtual environment, selected with `VIRTUAL_ENV` |

## Set up the project

```pkl
// elide.pkl
amends "elide:project.pkl"

name = "poly"

dependencies {
  npm { packages { "pretty-ms@9.3.1" } }
  pypi { packages { "humanize==4.12.3" } }
}
```

```bash
elide install --ecosystems=npm
```

A bare `elide install` installs Maven only. Elide does not install PyPI packages: `elide install --ecosystems=pypi`
exits `2` with `pypi installs are not available in this build`. Keep the `pypi` block as the record, and install it
with a Python 3.13 `pip` into a virtual environment:

```bash
python3.13 -m venv .venv
.venv/bin/pip install humanize==4.12.3
export VIRTUAL_ENV=$PWD/.venv
```

Elide loads packages from `$VIRTUAL_ENV/lib/python3.13/site-packages`. Pure-Python packages work; some C extension
modules do not load (see [Python](https://elide.help/docs/python#known-limitations)).

## Write the program

```python
# report.py
import humanize

def size(n):
    return humanize.naturalsize(n)

def summarize(files):
    total = sum(f.bytes for f in files)
    return {"count": len(files), "total": humanize.naturalsize(total)}
```

```javascript
// app.mjs
import prettyMs from "pretty-ms";
import report from "./report.py";

const files = [
  { name: "build.log", bytes: 1_500_000, ageMs: 172_800_000 },
  { name: "test.log", bytes: 32_000, ageMs: 5_400_000 },
];

for (const f of files) {
  console.log(`${f.name}: ${report.size(f.bytes)}, ${prettyMs(f.ageMs)} old`);
}

const summary = report.summarize(files);
console.log(`${summary.count} files, ${summary.total}`);
```

```bash
elide --interop run app.mjs
```

```text
build.log: 1.5 MB, 2d old
test.log: 32.0 kB, 1h 30m old
2 files, 1.5 MB
```

A Python file's default export is its module; import it from an `.mjs` entry. Two common failures:

| Missing | Error |
| --- | --- |
| `--interop` | `Error: Cannot import 'report.py': JavaScript entries run without Python. Re-run with --interop to use it.` |
| `VIRTUAL_ENV` | `ModuleNotFoundError: No module named 'humanize'` |

## Pass values

```python
# shapes.py
def label(order):
    return f"{order.item} x{order.qty}"

def doubled(prices):
    return [p * 2 for p in prices]

def config():
    return {"region": "eu", "retries": 3}

def apply(fn, value):
    return fn(value)

def fail():
    raise ValueError("bad input")
```

```javascript
// values.mjs
import shapes from "./shapes.py";

console.log(shapes.label({ item: "bolt", qty: 4 }));
console.log(shapes.doubled([1, 2.5, 3]).map((n) => n + 1));
const config = shapes.config();
console.log(config.region, config.get("retries"));
console.log(JSON.stringify(Object.fromEntries(config.items())));
console.log(shapes.apply((s) => s.toUpperCase(), "shout"));
console.log(shapes.apply((v) => v, null), shapes.apply((v) => v, undefined));
```

```bash
elide --interop run values.mjs
```

```text
bolt x4
(3)[3, 6, 7]
eu 3
{"region":"eu","retries":3}
SHOUT
null undefined
```

| Value | From JavaScript into Python | From Python into JavaScript |
| --- | --- | --- |
| Numbers, strings, booleans | Native values | Native values |
| `null`, `undefined` / `None` | `None`, and back unchanged | `null` |
| Object / `dict` | Read fields as attributes (`order.item`); `order["item"]` raises `TypeError` | Read keys as `d.key`, `d["key"]` or `d.get("key")`; `Object.keys`, `in` and `JSON.stringify` see the dict's methods, not its keys. Convert with `Object.fromEntries(d.items())`. |
| Array / `list` | Iterable, indexable, `len()` works | `Array.isArray` is `true`; `map`, spread and `Array.from` work |
| Function | Callable | Callable |

## Call JavaScript from Python

The `elide` module evaluates JavaScript and returns its value:

```python
# calls.py
from elide import js

slug = js("(s) => s.trim().toLowerCase().replaceAll(' ', '-')")
print(slug("  Hello Polyglot World "))

point = js("({ x: 1, y: 2, tags: ['a', 'b'] })")
print(point.x + point.y, point.tags[0], len(point.tags))
print(sorted(js("[3, 1, 2]")))

try:
    js("JSON.parse('{oops')")
except RuntimeError as e:
    print("caught:", e)

js("Promise.resolve(42)").then(lambda v: print("resolved:", v))
```

```bash
elide --interop python calls.py
```

```text
hello-polyglot-world
3 a 2
[1, 2, 3]
caught: JSException: SyntaxError: Unexpected token o in JSON at position 1
resolved: 42
```

## Handle errors

A Python exception reaches JavaScript as an `Error` whose `name` is the exception class; a JavaScript error reaches
Python as `RuntimeError`. An uncaught error prints one stack across both languages:

```javascript
// errors.mjs
import shapes from "./shapes.py";

try {
  shapes.fail();
} catch (e) {
  console.log(e.name, "|", e.message);
}

shapes.apply((x) => {
  throw new RangeError(`bad value ${x}`);
}, 7);
```

```bash
elide --interop run errors.mjs
```

```text
ValueError | ValueError: bad input
RangeError: bad value 7
    at :=> (errors.mjs:10:9)
    at apply (shapes.py:11:12)
    at :module:eval (errors.mjs:9:1)
```

The program exits `1`.

## Call async code

| From | Works | Does not work |
| --- | --- | --- |
| JavaScript calling Python | Call a sync wrapper that runs `asyncio.run(...)` | `await` on a Python coroutine returns the coroutine, unrun |
| Python holding a JavaScript promise | `promise.then(callback)` | `await promise` raises `TypeError: object polyglot.ForeignObject can't be used in 'await' expression` |

```python
# rates.py
import asyncio

async def fetch_rate(code):
    await asyncio.sleep(0.01)
    return {"EUR": 0.92, "GBP": 0.79}[code]

def rate(code):
    return asyncio.run(fetch_rate(code))

def when_ready(promise, done):
    promise.then(lambda value: done(f"python saw {value}"))
```

```javascript
// rates.mjs
import rates from "./rates.py";

console.log("rate:", rates.rate("EUR"));

const total = new Promise((resolve) => setTimeout(() => resolve(7), 10));
rates.when_ready(total, (msg) => console.log(msg));
await total;
```

```bash
elide --interop run rates.mjs
```

```text
rate: 0.92
python saw 7
```

`asyncio.run` blocks the JavaScript event loop until the coroutine finishes. Python threads are not available under
`--interop`.

## Java and Kotlin

`elide run Hello.kt` runs Kotlin on its own; JavaScript and Python cannot call it. Importing it fails:

```javascript
import hello from "./Hello.kt";
```

```text
TypeError: Unsupported file extension: 'file:/path/to/Hello.kt'
```

See also [Languages and Interop](https://elide.help/docs/languages), [Python](https://elide.help/docs/python) and [NPM Installer](https://elide.help/docs/toolchain/web/npm).

## Samples

- [Python calling JavaScript](https://elide.help/docs/code-samples#python-calling-javascript)
- [JavaScript importing Python](https://elide.help/docs/code-samples#javascript-importing-python)
