--- title: "Using Python inside targets" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Using Python inside targets} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` This vignette is the reference for running **Python** scripts as `targets` steps with [reticulate](https://rstudio.github.io/reticulate/): every `tar_target_py()` argument, how to choose the Python environment, and the common use cases. For a gentler tour start with `vignette("get_started")`; for Julia see `vignette("julia")`. Code blocks are illustrative and **not executed** when the vignette builds. ## The two constructors * `tar_target_py()`: bare `name` (and unquoted `pattern`), for direct use in `_targets.R`. * `tar_target_py_raw()`: string `name`, for use inside targets factories. Both return a single `targets` target and forward **every** `targets::tar_target_raw()` argument, so anything an ordinary target supports (`pattern`, `format`, `iteration`, `deployment`, `resources`, `cue`, `memory`, `error`, `priority`, `description`, ...) works exactly as usual. ## Arguments ### Scripts | Argument | Meaning | |---|---| | `script` | Path to the Python script to run (**required**). | | `pre_script` | Optional R script run **before** the Python script. Assign a named list `to_py` to push objects into Python's `__main__`. | | `post_script` | Optional R script run **after** the Python script. `py` and `py_get()` are available; its last expression is the value (object mode) or file paths (file mode). | Each of the three accepts either a **literal path string** (untracked: editing the file does not invalidate the target) or a `tar_target_path("name")` reference to an upstream `format = "file"` target (tracked: the step re-runs when the file changes). See "Tracking scripts" below. ### Wiring in data and reading it back | Argument | Meaning | |---|---| | `inputs` | Named character vector mapping the name seen in the step to an upstream target, e.g. `c(x = "prepared_x")`. Each becomes a real dependency (and is sliced under dynamic branching). | | `output` | `"object"` (default) returns a converted R object; `"file"` returns a character vector of paths and defaults `format` to `"file"`. | | `retrieve` | Character vector of Python variable names to return when there is **no** post-script (object mode). One name returns that object; several return a named list. | | `files` | Character vector of paths to return when there is **no** post-script (file mode). | ### Choosing the interpreter / environment | Argument | Meaning | |---|---| | `python` | Path to a specific interpreter executable. | | `env` + `env_manager` | Point at an existing environment built by a known tool. | | `python_version` | A version string (e.g. `"3.12"`); reticulate provisions it via an ephemeral uv environment. | These are **alternatives**: normally set only one. If several are given, the first in this precedence wins: **`python` > `env`/`env_manager` > `python_version` > nothing**. They are covered in detail in the next section. ## The three-script model ``` upstream targets ─► pre_script (R) ─► script (.py) ─► post_script (R) ─► target value builds `to_py` computes reads `py$name` / `result` `py_get("name")` ``` 1. **`script`**: the Python file. Runs in the `__main__` module. 2. **`pre_script`**: the `inputs` are already bound by name. Assign a named list `to_py`; each element is pushed as a top-level variable into `__main__` using reticulate's conversion rules. 3. **`post_script`**: `py` (the reticulate `__main__` proxy) and `py_get(name)` (an explicit `reticulate::py_to_r()` shortcut) are available. Read a variable with `py$result` or `py_get("result")`. ## Object output `py/compute.py`: ```python # `x` was pushed from R by the pre-script. seq = list(x) if hasattr(x, "__iter__") else [x] result = {"sum": float(sum(seq)), "n": len(seq), "mean": float(sum(seq)) / len(seq)} ``` `R/pre_push.R`: ```r to_py <- list(x = x) # `x` came from inputs = c(x = "prepared_x") ``` `R/post_result.R` (last expression becomes the value): ```r res <- py_get("result") data.frame(sum = res$sum, n = res$n, mean = res$mean) ``` `_targets.R`: ```r list( tar_target(prepared_x, c(1, 2, 3, 4)), # (a) return a Python variable directly with `retrieve` (no post-script) tar_target_py( name = py_direct, script = "py/compute.py", inputs = c(x = "prepared_x"), pre_script = "R/pre_push.R", retrieve = "result" ), # (b) reshape the result in a post-script tar_target_py( name = py_prepost, script = "py/compute.py", inputs = c(x = "prepared_x"), pre_script = "R/pre_push.R", post_script = "R/post_result.R" ) ) ``` ## File output Set `output = "file"` when the script writes to disk; return the path(s) from the post-script (or via `files` when there is no post-script). The target `format` becomes `"file"` automatically. ```r tar_target_py( name = py_file, script = "py/write_csv.py", # writes a CSV, stores its path in `out_path` inputs = c(x = "prepared_x"), pre_script = "R/pre_push.R", post_script = "R/post_files.R", # returns py_get("out_path") output = "file" ) ``` ## Dynamic branching `inputs` are real dependencies, so `pattern` branches them: ```r list( tar_target(chunks, list(10, 20, 30)), tar_target_py( name = py_branch, script = "py/compute.py", inputs = c(x = "chunks"), pre_script = "R/pre_push.R", retrieve = "result", pattern = map(chunks) # 3 branches: x = 10, 20, 30 ) ) ``` Run the branches in parallel by setting a `crew` controller (see `vignette("get_started")`, the "Parallelism and isolation with crew" section). ## Choosing the Python environment The four selection arguments and when to reach for each: * **`python`**: a path to a specific interpreter executable (e.g. `".venv/Scripts/python.exe"`). Use when you already have the exact interpreter and want no ambiguity. * **`env` + `env_manager`**: an *existing* environment built by a known tool (`env_manager` says how to read `env`; see the table). **Recommended for reproducible pipelines**: install the packages once, every run reuses them. * **`python_version`** (e.g. `"3.12"`), *not* an environment, just an interpreter *version*: reticulate provisions it through an *ephemeral*, uv-managed environment (`py_require()`), resolving packages on the fly. Use when only the version matters. * **nothing set**: reticulate's default discovery (the computer's Python, or an ephemeral uv-managed one on recent reticulate). Fine for exploration; pin an environment for reproducibility. The key contrast is **reproducible vs throwaway**: `env`/`python` reuse an environment *you* built and pinned; `python_version` lets reticulate build a fresh one keyed only on a version. One more difference: `python` and `env`/`env_manager` **override** an ambient `RETICULATE_PYTHON` (e.g. an RStudio project Python inherited by `crew` workers), while `python_version` and the default **respect** it. | `env_manager` | what `env` is | reticulate call | |---|---|---| | `"system"` (default) | none (optionally set `python`) | `use_python(python)` or default | | `"venv"` | a `python -m venv` directory path | `use_virtualenv(env)` | | `"virtualenv"` | same as venv | `use_virtualenv(env)` | | `"conda"` | conda environment name | `use_condaenv(env)` | | `"uv"` | a uv-created venv directory path | `use_virtualenv(env)` | | `"poetry"` | a poetry **project** directory path | `poetry env info -p`, then `use_virtualenv()` | `"virtualenv"`, `"venv"`, and `"uv"` all point at a standard Python virtualenv and behave identically: this includes one created by `renv::use_python()` (use `env_manager = "venv"` with the path under `renv/python/virtualenvs/`). For these managers, a **relative** `env` that is an existing directory is expanded to an absolute path, so `env = ".venv"` works; a bare name that is not a directory is passed through unchanged, so a genuine named environment still resolves. Setting `env` while `env_manager = "system"` is an error. ### Use case 1: global / system default Set nothing; reticulate uses the computer's default Python (on recent versions, an ephemeral uv-managed one provisioned on first use). Convenient for exploration; pin a real environment for reproducibility. ```r tar_target_py( name = probe, script = "py/probe.py", retrieve = "result" # no env / env_manager / python -> computer default Python ) ``` ### Use case 2: a pinned uv virtualenv (recommended) Create it once, then point to it. Reproducible: the exact package set is reused every run. ```sh uv venv .venv uv pip install --python .venv/Scripts/python.exe numpy pandas scikit-learn ``` ```r tar_target_py( name = fit, script = "py/fit.py", env_manager = "uv", env = ".venv", retrieve = "model" ) ``` ### Use case 3: a poetry project Give the **project directory** (the folder with `pyproject.toml`); its virtualenv is resolved for you with `poetry env info -p`. ```r tar_target_py( name = embed, script = "py/embed.py", env_manager = "poetry", env = "path/to/poetry_project", retrieve = "vectors" ) ``` If the `poetry` on `PATH` is not the one you want, select a specific executable with `options(tarpolyglot.poetry = "/path/to/poetry")`. ### Use case 4: a plain venv or a conda env ```r # python -m venv .venv tar_target_py( name = score, script = "py/score.py", env_manager = "venv", env = ".venv", retrieve = "result" ) # a named conda environment tar_target_py( name = classify, script = "py/classify.py", env_manager = "conda", env = "my_conda_env", retrieve = "labels" ) ``` ### Use case 5: pin just the version To fix the interpreter *version* without naming an environment, use `python_version` (leaves the environment at the default, uv-managed one): ```r tar_target_py( name = probe_ver, script = "py/probe.py", python_version = "3.12", # reticulate provisions 3.12 via uv retrieve = "result" ) ``` > **One interpreter per session.** reticulate binds a single Python per R session, so all Python targets that run in the same session share one environment. To use different environments in one pipeline, run those targets on separate `crew` workers (see `vignette("get_started")`). ## Tracking scripts as dependencies By default `script` / `pre_script` / `post_script` are untracked literal paths. To have the step re-run when a script changes, track it with a `format = "file"` target and reference it with `tar_target_path()`: ```r list( tar_target(fit_py, "py/fit.py", format = "file"), tar_target_py( name = fit, script = tar_target_path("fit_py"), # re-runs when py/fit.py changes pre_script = "R/pre_fit.R", # still an untracked literal path inputs = c(x = "data"), retrieve = "model" ) ) ``` ## Conversion caveats * A bare number in R is a double; use `L` (e.g. `42L`) when Python needs an int. * Python is 0-indexed. * For anything that does not round-trip cleanly, use file mode. See `?tar_target_py` and `?run_py_step` for the full argument reference.