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
NAis stillNAgoing 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.
numpyis the only requirement. R needs thejsonlitepackage; 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 aRuntimeErrorhere with the other side's message, and the session answers the next call.- A child that ends — a crash, a
quit()— is aChildProcessError, and the next call starts another. - What the called code prints, on either stream, goes to the
locutorlogger atINFO, 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.
ordinatordoes 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.