{tarpolyglot}

{tarpolyglot} adds target constructors that run Python, Julia, Rust, and C++ as first-class steps of a {targets} pipeline, via reticulate (Python), JuliaCall (Julia), rextendr / extendr (Rust), and Rcpp (C++). Results come back as converted R objects or as files tracked on disk, and everything a normal {targets} step supports works unchanged: dynamic branching, storage formats, resources, cues, crew parallelism.

Installation

You can install {tarpolyglot} from CRAN or GitHub for the development version:

# CRAN
install.package("tarpolyglot")

# GitHub (development version)
# install.packages("remotes")
remotes::install_github("Pierre9344/tarpolyglot@dev", build_vignettes = TRUE, dependencies = TRUE)

You also need a working toolchain for whichever language(s) you plan to use: Python, Julia, and/or Rust. Each is independent and only needed if you use the matching constructor (tar_target_py(), tar_target_jl(), tar_target_rs()). See vignette("get_started") for the full setup instructions (Python via reticulate/uv, Julia via juliaup, Rust via rustup: use the GNU toolchain on Windows so it matches R's ABI). C++ needs no separate toolchain since Rcpp::sourceCpp() compiles with R's own compiler, which on Windows means Rtools).

The three constructors

Each mirrors targets::tar_target() / targets::tar_target_raw() and forwards every argument (pattern, format, deployment, resources, cue, ...), so dynamic branching, storage formats, crew-based parallelism, and other {targets} features all work as usual.

Example using tar_target_py

A minimal pipeline: an R target feeds a Python step, which returns a value that a downstream R target uses.

scripts/pre.R:

to_py <- list(x = x)   # `x` is bound from inputs = c(x = "values")

scripts/mean.py:

# `x` is handed over by the pre-script; assign `result` for R to read.
result = {"mean": sum(x) / len(x), "n": len(x)}

_targets.R:

library(targets)
library(tarpolyglot)

# Recommended default: run each step in its own worker process so every
# Python/Julia step gets a fresh interpreter. See the "crew" vignette.
tar_option_set(controller = polyglot_controller(workers = 1L))

list(
  tar_target(values, c(2, 4, 6, 8)),

  tar_target_py(
    name = py_mean,
    script = "scripts/mean.py",
    inputs = c(x = "values"),
    pre_script = "scripts/pre.R",
    retrieve = "result"           # returns the Python `result` dict as an R list
  ),

  tar_target(report, sprintf("mean = %.1f over n = %d", py_mean$mean, py_mean$n))
)

We can run and inspect it like any {targets} pipeline:

targets::tar_make()
targets::tar_read(py_mean)   # $mean 5  $n 4
targets::tar_read(report)    # "mean = 5.0 over n = 4"

Here the retrieve argument is used to extract a value from the python environment using reticulate type conversion. It is also possible to use a post-python R script to extract one or more values from the python session (e.g. save multiple object as a list, or format the output in R). In the post script, the python session is represented by the py object. It is also possible tu use the py_get() helper function to extract some variable. Both are created by the steps constructor.

Inline code with tar_code()

The script, pre_script, and post_script arguments do not have to point at a file on disk. Wrap code in tar_code() to write it directly in _targets.R instead: an R { } block supplies inline R for a pre_script or post_script, and a character string supplies inline source for the foreign script (Python, Julia, or Rust) or for an R pre/post-script. The Python example above can be written entirely inline:

tar_target_py(
  name = py_mean,
  inputs = c(x = "values"),
  pre_script = tar_code({ to_py <- list(x = x) }),
  script = tar_code("result = {'mean': sum(x) / len(x), 'n': len(x)}"),
  retrieve = "result"
)

Multi-line strings work too and are dedented, so you can indent the code to line up with the surrounding _targets.R while Python's own block indentation is preserved. Inline code is embedded in the target's command, so {targets} hashes it and re-runs the step whenever you edit it (a literal file-path string is untracked unless you wrap it in tar_target_path()).

Julia and Rust similarity and differences

A Julia step is identical apart from the constructor and helper names: use tar_target_jl(), a .jl script, to_jl in the pre-script, and jl_get() in a post-script. A Rust step has no pre-script: compile #[extendr] functions and call them from the post-script. For Rust in particular, a plain tar_target() calling rextendr::rust_source() is often enough; tar_target_rs() mainly adds toolchain and crew-worker build setup plus API symmetry (see vignette("rust")).

Why use {tarpolyglot} instead of calling the toolchains directly

The gains above are largest for Python and Julia, where a live interpreter has to be bound and data marshalled across the R boundary on every step. Rust is different: rextendr::rust_source() compiles your #[extendr] functions and hands them back as ordinary R functions, so there is no interpreter to bind and no conversion layer to abstract, and for simple cases a plain tar_target() calling rust_source() already works. tar_target_rs() is still worth using because it puts R, cargo, and (on Windows) Rtools on PATH and sets R_HOME so the build succeeds in a bare or crew worker, lets you pick a toolchain per step with toolchain, bundles file output, script tracking, and the full targets::tar_target_raw() argument set in one call, and keeps the same API as the Python and Julia constructors (see vignette("rust")).

Comparison with other similar tools

Comparison to {rixpress}

I am also aware of {rixpress}, which supports reproducible polyglot pipelines using Nix.

I see {rixpress} and {tarpolyglot} as addressing related needs with different architectures and trade-offs. {rixpress} delegates pipeline execution, dependency management, and environment management to Nix, providing strong isolation and reproducibility guarantees.

{tarpolyglot} takes a narrower and less prescriptive approach. It remains an extension of {targets} and leaves environment management to existing language-specific tools such as virtual environments, Conda, uv, Poetry, Juliaup, Julia projects, and rustup.

The intention is not to reproduce the reproducibility guarantees provided by Nix. Instead, {tarpolyglot} is intended for users who want to add multilingual steps incrementally to an existing {targets} pipeline, including environments where Nix is unavailable or cannot be installed.

This distinction is relevant for my own work because I use a public computing cluster with restricted user permissions, and the administrators are not currently considering a Nix installation.

Comparison with T (tlang)

I am also aware of T, a domain-specific language for orchestrating polyglot analyses, currently in active development.

T and {tarpolyglot} differ more fundamentally than {rixpress} and {tarpolyglot} do, because T is a language rather than an R package. T owns the orchestration layer itself: pipelines are written in T, which coordinates R, Python, and Julia as computation backends, serialises artifacts across language boundaries automatically, and runs each node in its own sandboxed environment. Its design is strictly functional and immutable, with computation graphs as first-class values.

{tarpolyglot} does not own the orchestration layer and does not try to. The pipeline stays an ordinary {targets} pipeline written in R, and a multilingual step is one more constructor call in _targets.R. Everything a user already knows about {targets} (change detection, dynamic branching, storage formats, crew parallelism) continues to apply unchanged, and nothing outside the individual step needs to move.

The trade-off is the same shape as with {rixpress}, and in the same direction. T offers stronger guarantees: mandatory Nix environments, per-node sandboxing, and automatic cross-language serialisation remove classes of error that {tarpolyglot} leaves to the user's own environment management. In exchange, T asks the user to adopt a new language and to have Nix available, whereas {tarpolyglot} asks for one function call inside a pipeline they already have.

The two therefore suit different starting points. T is attractive when a polyglot workflow is being designed from scratch and reproducibility guarantees are the priority. {tarpolyglot} is aimed at an existing R-centric {targets} pipeline that needs one or two steps in another language, and like for {rixpress} to the people for which installing Nix is not an option.

Where to go next

AI Disclosure

Claude Code (Anthropic's Claude Opus 4.8, and then Opus 5) was used as an assistant throughout development, with the maintainer reviewing, testing, and directing all changes made by the AI. Concretely, AI assistance was used for:

Citation

To cite {tarpolyglot}, run citation("tarpolyglot") in R, or see the CITATION.cff file (also surfaced by GitHub's "Cite this repository" button).