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.3.0.tar.gz (110.8 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.3.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: r_bridge-0.3.0.tar.gz
  • Upload date:
  • Size: 110.8 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.3.0.tar.gz
Algorithm Hash digest
SHA256 303f870fa97972df0392ca660f60433624a35c90515d6a22d4b4be1e4e0e1a4c
MD5 ecfe145a44e947d10ac41a27a01a1d1b
BLAKE2b-256 9c3b4df9a8fc74d73714fbd887c6958b9eb999d692621374dabc8fb2c0aabfa9

See more details on using hashes here.

File details

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

File metadata

  • Download URL: r_bridge-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 19.6 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.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c6bb4c2f03ed7ffacd04dfe58a962748592abd058db0ec747709993e01e836a8
MD5 5b8c7f0099e1d3fe9fcac1de124aec87
BLAKE2b-256 9a1f4fe13ac99b1625c02b84c63a4951906bf266c3d1d5d70f546c9ee33eeeae

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