Managing Packages
Last updated on 2026-10-23 | Edit this page
Overview
Questions
- How should I manage packages for my
targetsproject?
Objectives
- Demonstrate best practices for managing packages
Loading packages
Almost every R analysis relies on packages for functions beyond those available in base R.
Our workflow already does: read_csv() comes from the
readr package, and clean_penguin_data() uses
select() from dplyr, and
drop_na() and separate() from
tidyr. All of these are part of the
tidyverse.
Here is our workflow so far:
R
source("R/packages.R")
source("R/functions.R")
tar_plan(
penguins_csv_file = "data/penguins_raw.csv",
penguins_data_raw = read_csv(penguins_csv_file, show_col_types = FALSE),
penguins_data = clean_penguin_data(penguins_data_raw)
)
What we have done so far works: the library() calls in
R/packages.R make every function we need available, so why
would we load packages any other way? For a small workflow like ours,
there is no need to. But as a workflow grows, loading packages with
library() can start to cause some problems:
-
It can get slow. All of the packages are loaded
every time
targetsreads your_targets.Rfile (for example, withtar_make()ortar_visnetwork()), whether or not the targets that are about to run actually need them. With a long list of packages, that adds up. - It gets harder to tell where functions come from. When many packages are loaded, it is not obvious which package a given function belongs to, and two packages may have functions with the same name (we will come back to this at the end of the episode).
targets offers some other ways to make packages
available that can help with these problems, and you will see them in
other people’s targets projects.
There are three main ways to load packages in targets
workflows, plus one way to avoid loading them. Below we will see what
our workflow would look like with each one. These are examples to read:
you do not need to change your own files (the method you already have is
the first one).
Method 1: library()
This is the method you are almost certainly more familiar with, and
is the method we have been using by default so far:
R/packages.R loads the packages with
library(), and _targets.R runs
source("R/packages.R").
R
library(targets)
library(tarchetypes)
library(tidyverse)
Like any other R script, you can include library() calls
near the top of the _targets.R script. Alternatively (and
as the recommended best practice for project
organization), you can put all of the library() calls
in a separate script—this is typically called packages.R
and stored in the R/ directory of your project.
The potential downside to this approach is that if you have a long
list of packages to load, certain functions like
tar_visnetwork() may take an unnecessarily long time to run
because they have to load all the packages, even though they don’t
necessarily use them.
Method 2: tar_option_set()
In this method, use the tar_option_set() function in
_targets.R to specify the packages to load when running the
workflow.
For our workflow, we could remove library(tidyverse)
from R/packages.R (so that it only has
library(targets) and library(tarchetypes)),
then list the packages we need in _targets.R instead:
R
source("R/packages.R")
source("R/functions.R")
tar_option_set(packages = c("readr", "dplyr", "tidyr"))
tar_plan(
penguins_csv_file = "data/penguins_raw.csv",
penguins_data_raw = read_csv(penguins_csv_file, show_col_types = FALSE),
penguins_data = clean_penguin_data(penguins_data_raw)
)
OUTPUT
+ penguins_csv_file dispatched
✔ penguins_csv_file completed [104ms, 74 B]
+ penguins_data_raw dispatched
✔ penguins_data_raw completed [113ms, 10.40 kB]
+ penguins_data dispatched
✔ penguins_data completed [23ms, 1.49 kB]
✔ ended pipeline [666ms, 3 completed, 0 skipped]
This method gets around the slow-downs that may sometimes be experienced with Method 1.
Method 3: packages argument of
tar_target()
The main function for defining targets, tar_target()
includes a packages argument that will load the specified
packages only for that target.
Here is how our workflow could use this method (again with
library(tidyverse) removed from R/packages.R).
penguins_data_raw needs readr, and
penguins_data needs dplyr and
tidyr.
R
source("R/packages.R")
source("R/functions.R")
tar_plan(
penguins_csv_file = "data/penguins_raw.csv",
tar_target(
penguins_data_raw,
read_csv(penguins_csv_file, show_col_types = FALSE),
packages = "readr"
),
tar_target(
penguins_data,
clean_penguin_data(penguins_data_raw),
packages = c("dplyr", "tidyr")
)
)
OUTPUT
+ penguins_csv_file dispatched
✔ penguins_csv_file completed [1ms, 74 B]
+ penguins_data_raw dispatched
✔ penguins_data_raw completed [178ms, 10.40 kB]
+ penguins_data dispatched
✔ penguins_data completed [60ms, 1.49 kB]
✔ ended pipeline [675ms, 3 completed, 0 skipped]
This can be more memory efficient in some cases than loading all packages, since not every target is always made during a typical run of the workflow. But, it can be tedious to remember and specify packages needed on a per-target basis.
One more option
Another alternative that does not actually involve loading packages
is to specify the package associated with each function by using the
:: notation, for example, dplyr::mutate().
This means you can avoid loading packages
altogether.
Here is how to write the plan using this method, along with the
custom function clean_penguin_data() in
R/functions.R. We no longer need R/packages.R
at all; the only packages we load are targets and
tarchetypes, which provide tar_plan()
itself:
R
library(targets)
library(tarchetypes)
source("R/functions.R")
tar_plan(
penguins_csv_file = "data/penguins_raw.csv",
penguins_data_raw = readr::read_csv(penguins_csv_file, show_col_types = FALSE),
penguins_data = clean_penguin_data(penguins_data_raw)
)
R
clean_penguin_data <- function(penguins_data_raw) {
penguins_data_raw |>
dplyr::select(
species = Species,
bill_length_mm = `Culmen Length (mm)`,
bill_depth_mm = `Culmen Depth (mm)`
) |>
tidyr::drop_na() |>
# Split "species" apart on spaces, and only keep the first word
tidyr::separate(species, into = "species", extra = "drop")
}
OUTPUT
+ penguins_csv_file dispatched
✔ penguins_csv_file completed [1ms, 74 B]
+ penguins_data_raw dispatched
✔ penguins_data_raw completed [177ms, 10.40 kB]
+ penguins_data dispatched
✔ penguins_data completed [49ms, 1.49 kB]
✔ ended pipeline [645ms, 3 completed, 0 skipped]
The benefits of this approach are that the origins of all functions is explicit, so you could browse your code (for example, by looking at its source in GitHub), and immediately know where all the functions come from. The downside is that it is rather verbose because you need to type the package name every time you use one of its functions.
Which is the right way?
There is no “right” answer about how to load packages—it is a matter of what works best for your particular situation.
Often a reasonable approach is to load your most commonly used
packages with library() (such as tidyverse) in
packages.R, then use :: notation for less
frequently used functions whose origins you may otherwise forget.
This is the approach we use in this lesson. The
tidyverse is loaded with library() in
R/packages.R, and we use :: for packages we
only need in one place, like broom::glance() in the
branching episode.
Maintaining package versions
Tracking of custom functions vs. functions from packages
A critical thing to understand about targets is that
it only tracks custom functions and targets, not
functions provided by packages.
However, the content of packages can change, and packages typically get updated on a regular basis. The output of your workflow may depend not only on the packages you use, but their versions.
Therefore, it is a good idea to track package versions.
About renv
Fortunately, you don’t have to do this by hand: there are R packages
available that can help automate this process. We recommend renv, but there are
others available as well (e.g., groundhog). We don’t have the time to
cover detailed usage of renv in this lesson. To get started
with renv, see the “Introduction
to renv” vignette.
You can generally use renv the same way you would for a
targets project as any other R project. renv
detects packages loaded with library(),
require(), etc., and also packages listed in the
packages argument of tar_option_set() in
_targets.R (Method 2).
However, there is one exception: if you load packages using the
packages argument of tar_target() (Method 3), renv will not detect
them.
The solution in this case is to use the tar_renv()
function. This will write a separate file with
library() calls for each package used in the workflow so
that renv will properly detect them.
Selective tracking of functions from packages
Because targets doesn’t track functions from packages,
if you update a package and the contents of one of its functions
changes, targets will not re-build the target that
was generated by that function.
However, it is possible to change this behavior on a per-package
basis. This is best done only for a small number of packages, since
adding too many would add too much computational overhead to
targets when it has to calculate dependencies. For example,
you may want to do this if you are using your own custom package that
you update frequently.
The way to do so is by using tar_option_set(),
specifying the same package name in both
packages and imports. Here is a modified
version of the Method 2 example that demonstrates this for
readr and tidyr.
R
tar_option_set(
packages = c("readr", "dplyr", "tidyr"),
imports = c("readr", "tidyr")
)
If we were to re-install either readr or
tidyr and one of the functions used from those in the
pipeline changes (for example, read_csv()), any target
depending on that function will be rebuilt.
Note that this does not work for every package: for example, asking
targets to track dplyr this way fails with an
error about circular dependencies. That is why we only track
readr and tidyr here.
Resolving namespace conflicts
There is one final best-practice to mention related to packages: resolving namespace conflicts.
“Namespace” refers to the idea that a certain set of unique names are only unique within a particular context. For example, all the function names of a package have to be unique, but only within that package. Function names could be duplicated across packages.
As you may imagine, this can cause confusion. For example, the
filter() function appears in both the stats
package and the dplyr package, but does completely
different things in each. This is a namespace conflict:
how do we know which filter() we are talking about?
One solution we have covered already is using the ::
notation when calling functions. This is because writing
dplyr::filter() specifies that you are using
filter() from the dplyr namespace. The
tradeoff to this approach is verbosity, meaning more typing and
reading.
Another solution is to use the conflicted
package, which stops you with an error if you use a function whose name
is ambiguous, so that you can choose which one you mean. We will not go
into the details here.
- There are multiple ways to load packages with
targets -
targetsonly tracks user-defined functions, not packages - Use
renvto manage package versions - Use the
::notation (for example,dplyr::filter()) to be explicit about which package a function comes from and avoid namespace conflicts