Skip to main content

locutor

Call R and Julia from Python: a function in memory, or a script in a process.

import locutor

# a function: one persistent child per language, nothing written to disk
rotated = locutor.call("methods/rotation.R:double_rotation", u, v, w)
total = locutor.call("stats.jl:total", series, skip_missing=True)

# a script: a process of its own, arguments on its command line
locutor.run("fetch.R", "2026-11-13", out="data/raw.csv")

The name is the Latin for one who speaks. It is the sibling of regorator, which registers, and of ordinator, which orders: this one speaks to the languages the other two are not written in.

What it is for

A function written in R or Julia that you want to call from Python many times, with arrays, and get the same numbers back — without embedding the other language in the Python process.

  • In memory. One child per language is started at the first call and kept. No file is written and no process started for a call.
  • Exact. A double is never printed and parsed. Every float crosses as its 64 bits, so a NaN arrives as it left, payload included, and R's NA is still NA going back.
  • Separate. The other language runs in its own process. A crash there is an exception here, and its threads, signal handlers and C libraries never meet Python's.
  • No dependency on either language. numpy is the only requirement. R needs the jsonlite package; Julia needs nothing. A language that is not installed is refused at its first call, with what to install.

And the plain case beside it: a script that takes a command line and exits.

Install

pip install locutor

or, from a checkout, pip install -e ..

Python >= 3.9 and numpy. Rscript and julia are looked for on PATH.

Calling a function

locutor.call("path/file.ext:function", *args, **kwargs) picks the language by the file's extension and uses that language's shared session.

import numpy as np, locutor

x = np.arange(4.0)
locutor.call("lib.R:twice", x, by=3)      # array([0., 3., 6., 9.])
locutor.call("lib.jl:twice", x, by=3)     # the same, from Julia
locutor.call("lib.py:twice", x, by=3)     # the same, imported in this process

The child reads a file once, into a namespace of its own, so two files may define a function of one name. Keyword arguments are keyword arguments in Julia and named arguments in R.

For a package's function, a file of helpers, or another interpreter than the one on PATH, configure the language and call that:

with locutor.R(packages=["stats"], include="helpers.R") as r:
    r.call("stats::median", x)            # a package's function
    r.call("my_helper", x, window=5)      # a function helpers.R defines

jl = locutor.Julia(program="/opt/julia-1.11/bin/julia", packages=["Statistics"])
jl.call("Statistics.mean", x)
jl.close()

Building one starts nothing; its child starts at the first call and ends at close(), at the end of the with, or when Python exits.

What crosses

Python R Julia
a float a numeric vector of one a Float64
a numeric array a numeric vector a Vector{Float64}
an N-d array a matrix or array a Matrix / Array
an int, a bool, a str an integer, a logical, a character an Int, a Bool, a String
None NULL nothing
a list of one kind of those the vector of that kind the vector of that kind
a dict a named list a named tuple
any other list a list a Vector{Any}

Coming back, a named list or named tuple is a Python list that carries .names; a matrix keeps its shape; NA, NaN and Julia's missing in a float array are all NaN. Anything else — a function, a struct, a DataFrame, an xarray object going the other way — is refused by name rather than converted to whatever its values happen to coerce to.

Errors and output

  • stop() in R or an exception in Julia is a RuntimeError here with the other side's message, and the session answers the next call.
  • A child that ends — a crash, a quit() — is a ChildProcessError, and the next call starts another.
  • What the called code prints, on either stream, goes to the locutor logger at INFO, and never into an answer.

Running a script

locutor.run(script, *arguments, **options) runs it with the interpreter its extension names and waits. Positional arguments go on the command line as they are; keyword options go as --key value (True is the bare flag, a list is the key followed by its values).

done = locutor.run("report.jl", "FR-Gri", year=2022, sites=["a", "b"])
done.returncode, done.stdout, done.stderr

A status other than zero raises RuntimeError with the end of the script's error stream; check=False returns the Result instead. capture=False leaves the script's output on this process's streams. cwd, env and timeout are passed through.

From a shell

locutor languages
locutor run script.R 2026-11-13 --out data/raw.csv
locutor call stats.jl:total "[1, 2.5, 4]" --skip_missing true

run hands everything after the script to the script, untouched, and exits with its status. call reads each value as JSON where it is JSON and as text where it is not, and prints the answer as JSON.

How it works

A session is one child process (Rscript, julia) running locutor/servers/session.R or session.jl. A request is one JSON line on its input and the answer one JSON line on its output; the doubles of both travel as raw little-endian float64 on one loopback TCP connection the child opens back when it starts. The lines and the doubles go separate ways because the child's output is also where the called code prints: an answer line is marked, and raw doubles on that stream would be corrupted by the first print. The connection is 127.0.0.1 only, and is taken only from the child that repeats a token it was handed on its input.

Measured on Linux (R 4.5.3, Julia 1.11.7), one 36 000-sample series there and back: 0.16 ms for R and 0.09 ms for Julia, against 0.17–0.25 ms for rpy2 and 0.05 ms for juliacall embedded in the interpreter. Starting the child takes about 0.1 s for R and about 1 s for Julia including its first compilation.

Adding a language is one module in locutor/languages/ (what it is called, how it is started, how a package is installed) and one file in locutor/servers/ that holds its end of the conversation.

What it is not

  • Not a workflow tool. It does not order steps, read a configuration, or record what ran. ordinator does that, and is meant to call this.
  • Not an embedding. For sharing objects between languages without copying, or calling back into Python from R or Julia, use rpy2 or juliacall.
  • Not tested on Windows or macOS yet. Everything here was run on Linux.

Tests

python -m pytest

The tests that need R or Julia run once per language installed and skip the other, saying what to install.

Licence

EUPL-1.2. See LICENCE.

Metadata

Release files for locutor 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for locutor 0.1.0
File Size Uploaded Provenance Actions
locutor-0.1.0.tar.gz 38.8 kB Without provenance Details for locutor-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for locutor 0.1.0
File Interpreter ABI Platform Actions
locutor-0.1.0-py3-none-any.whl Python 3 none any Details for locutor-0.1.0-py3-none-any.whl

Total release size: 75.3 kB

Release files / locutor-0.1.0.tar.gz

Download URL locutor-0.1.0.tar.gz
Size 38.8 kB
Tags Source
SHA-256 checksum
How to use checksums
519828daad15afa7c1441c890fe5fda07bda4feaa6f01d50bd3cf6ccd7f1baf7
BLAKE2b-256 checksum
How to use checksums
b4dc4b1df28232ad65c0102874d8a46a47c9ac4c6e8e7d342247518a9f491990
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.15.2

Release files / locutor-0.1.0-py3-none-any.whl

Download URL locutor-0.1.0-py3-none-any.whl
Size 36.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
db4652db8f3113563b8a4e40941e256f382e2082a27bde04684929b3c14e4e45
BLAKE2b-256 checksum
How to use checksums
c07f66c2ec1db895f244443c01c2e3b205984eee7c8d72acfd418e9446e3cadc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.15.2

Security for locutor 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page