Skip to main content

Call a persistent R interpreter from Python

Project description

🌉 r_bridge

Call a persistent R interpreter from Python — with a Pythonic API, full numpy/pandas support, and zero ceremony.

from r_bridge import RBridge

with RBridge() as r:
    r.x = [1, 2, 3, 4, 5]
    print(r.mean(r.x))   # 3.0

✨ Why r_bridge?

Python and R each have unique strengths. Rather than rewriting R code or shelling out to Rscript for every call, r_bridge keeps a single R process alive for the lifetime of your session. Calls are fast, state is shared, and the API feels natural on both sides.

  • 🚀 No subprocess-per-call overhead — R starts once, stays running
  • 🔄 Automatic type conversion — Python lists, numpy arrays, pandas DataFrames, datetimes all round-trip transparently
  • 🔒 Thread-safe — a single lock serialises concurrent calls; safe to use from threads
  • 🛡️ Robust — a sentinel-prefixed protocol means stray cat() output never corrupts the stream

📦 Installation

Requires Python ≥ 3.11 and R installed on your PATH.

pip install r-bridge

🪟 Windows note: R 4.2+ (UCRT build) is required for correct UTF-8 handling.


🚀 Quick start

from r_bridge import RBridge
import pandas as pd
import numpy as np

with RBridge() as r:
    # Set and get variables
    r.name = "world"
    print(r.paste("hello", r.name))   # "hello world"

    # Vectors and numpy arrays
    r.v = np.linspace(0, 1, 100)
    print(r.mean(r.v))                # 0.5

    # DataFrames
    r.df = pd.DataFrame({"x": [1, 2, 3], "y": [4.0, 5.0, 6.0]})
    result = r.df                     # returns a pandas DataFrame

    # Arbitrary R expressions
    r.eval("model <- lm(y ~ x, data=df)")
    coefs = r.coef(r.model)

    # Keyword arguments become named R arguments
    r.seq(1, 10, by=2)               # [1, 3, 5, 7, 9]

📖 API reference

RBridge(...) — constructor

Parameter Default Description
r_executable None Path to Rscript; auto-detected from PATH if omitted
startup_timeout 15.0 Seconds to wait for R ready signal
call_timeout 60.0 Per-call timeout in seconds
env {} Extra environment variables for the R subprocess
r_libs [] Paths prepended to R_LIBS_USER
log_level "WARNING" Python logging level for the r_bridge logger
verbose False Print all R calls and R output to stderr in real time

Attribute access (primary interface)

r.x = value          # set a variable in R's global environment
value = r.x          # get a variable from R
r.mean([1, 2, 3])    # call an R function; returns a Python value

Explicit methods

r.set("x", value)                          # set with optional type_hint
r.get("x", result_type_hint="scalar")      # get with type conversion hint
r.call("mean", [1,2,3], timeout=5.0)       # call with per-call timeout
r.eval("x <- 1 + 1", result_type_hint=None)
r.ls()                                     # list names in R global env
r.ping()                                   # round-trip latency in seconds
r.last_stderr                              # accumulated R stderr as string

result_type_hint values

Hint Effect
"scalar" Return first element as a Python scalar
"numpy" Return as numpy.ndarray
"pandas" Return as pandas.Series or DataFrame
"list" Return as Python list
"raw" Return the raw tagged JSON dict, no conversion

🔀 Type conversion

Python → → R
int, float, bool, str, None scalar atomic / NULL
list (homogeneous) atomic vector
dict named list()
numpy.ndarray 1-D atomic vector
numpy.ndarray 2-D matrix
pandas.DataFrame data.frame
pandas.Categorical factor
datetime.datetime POSIXct
datetime.date Date

↕️ All conversions are bidirectional. Special R values (NA, NaN, Inf, factor, matrix, …) all round-trip correctly.


🔍 Verbose mode

Set verbose=True to trace every R call and see R's output in real time — great for debugging.

with RBridge(verbose=True) as r:
    r.x = [1, 2, 3]
    r.mean(r.x)
[R set]    x = {'__type__': 'integer_vector', 'value': [1, 2, 3]}
[R call]   mean([...])
[R stdout] ...    ← from cat() in R
[R stderr] ...    ← from message() or warnings in R

⚠️ Error handling

from r_bridge.exceptions import RError, RTimeoutError

try:
    r.eval("stop('something went wrong')")
except RError as e:
    print(e.message)    # "something went wrong"
    print(e.warnings)   # list of captured R warnings
    print(e.traceback)  # R traceback as list of strings

try:
    r.eval("Sys.sleep(99)", timeout=1.0)
except RTimeoutError:
    print("R took too long")

Exception hierarchy

RBridgeError          base class
├── RStartupError     R failed to start
├── RTimeoutError     call exceeded timeout
├── RError            R-side error (carries .message, .call, .traceback, .warnings)
└── RProtocolError    malformed response envelope

🔌 Custom type converters

Register serializers and deserializers without modifying the library:

from r_bridge import register_serializer, register_deserializer

register_serializer(MyType, lambda obj: {"__type__": "mytype", "value": obj.to_dict()})
register_deserializer("mytype", lambda d: MyType.from_dict(d["value"]))

⚙️ How it works

r_bridge spawns Rscript once and communicates over stdin/stdout using line-delimited JSON with a __RBRIDGE__: sentinel prefix on every protocol line. Stray cat() or print() output is captured and logged rather than corrupting the stream.

🐍 Python ──► __RBRIDGE__:{"id":"…","op":"call_func","payload":{…}}
📊 R      ──► __RBRIDGE__:{"id":"…","status":"ok","payload":{…},"warnings":[]}

A dedicated daemon thread drains R's stderr continuously to prevent pipe-buffer deadlocks. A single threading.Lock serialises concurrent Python calls.


🧪 Development

uv run pytest                          # all tests
uv run pytest tests/test_bridge.py -v  # integration tests (spawns real R)
uv run pytest tests/test_serializer.py # unit tests (no R needed)

📄 License

MIT

Project details


Download files

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

Source Distribution

r_bridge-0.4.0.tar.gz (85.5 kB view details)

Uploaded Source

Built Distribution

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

r_bridge-0.4.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

Details for the file r_bridge-0.4.0.tar.gz.

File metadata

  • Download URL: r_bridge-0.4.0.tar.gz
  • Upload date:
  • Size: 85.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.11

File hashes

Hashes for r_bridge-0.4.0.tar.gz
Algorithm Hash digest
SHA256 12889a1ebddce8a7028bc90d31c0efef7da433e58755c97902a464e966b08432
MD5 fbece4a4e9a520fd0d48cc84d5e2c667
BLAKE2b-256 cd2f72f00831b84826910eeb1aa981893a1cddce6341cedcf5804e4b6ea7e3f9

See more details on using hashes here.

File details

Details for the file r_bridge-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: r_bridge-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 20.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.11

File hashes

Hashes for r_bridge-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e5899c1fcf9d6589c3af9b0127cbbaa35f31896933d231fac0eeb7d0bf58a9c0
MD5 1a31b1a8ba57f69420a7107c1403fa72
BLAKE2b-256 5a9aecda8590961a58f9088dbf6e7473718e9e4200e615e4eb47cbf17a393959

See more details on using hashes here.

Supported by

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