Isn’t it great that R has more than 1 solution to load packages? Some of them are beautiful. Some of them should be illegal in at least three countries. Let’s rank them from “please never do this” to “finally, some good food.”

In this post, I will try enumerate the different ways to load packages in R, and discuss their pros and cons. I will also rank them from worst to best solution in practices.
1 Different ways to load / attach packages in R
I sorted the different ways to load packages in R from worst to best solution in practices. This may be subjective, but I will try to justify my ranking based on the principles of good programming practices.
This is how I rank them from worst to best:
| Rank | Method | Verdict |
|---|---|---|
| 9 | base::use() |
Experimental copium (now officially dead last) |
| 8 | require() |
War criminal |
| 7 | {pacman} |
Convenience trap |
| 6 | library() |
Boomer energy |
| 5 | library() + conflicted |
Manual gearbox in 2025 |
| 4 | :: everywhere |
Professional wrist pain |
| 3 | {import} |
Extremely polite gentleman |
| 2 | {box} |
The chosen one |
| 1 | {box} + coffee |
Ascended plane of existence |
1.1 The new base::use() function (v4.4.0+)
Update: When I discover the bug, thanks to u/guepier and his comment, this changes my mind. Now, I put this in the worst place amongst the solution I listed here.
I feel like R Core saw the chaos and said “fine, we’ll do something”. This function is available in R version 4.4.0 and above, by the way.
It allows you to load packages in a way that minimizes namespace conflicts by only attaching the functions you explicitly use. Take note that base::use() is a short case of library(), a simple wrapper, where it keeps include.only and set:
-
lib.loctoNULL -
character.onlytoTRUE -
logical.returntoTRUE -
attach.requiredtoFALSE
This is still library(), but granular imports are explicit. Except…

Another problem occurs: Remember, it is just a simple wrapper of library(), therefore the import still goes to the search path.
It’s like putting a fancy new paint job on a 1987 Honda Civic and calling it a Ferrari. It LOOKS different, but under the hood, same old engine, baby.
For example:
mean_data = function(.data) {
use('dplyr', 'summarise')
use('tidyr', 'pivot_longer')
summarise(
.data, across(
where(is.numeric),
\(col) mean(col, na.rm = TRUE)
)
) |>
pivot_longer(
cols = where(is.numeric),
names_to = "Variable",
values_to = "Ave"
)
}
mean_data(iris)# A tibble: 4 × 2
Variable Ave
<chr> <dbl>
1 Sepal.Length 5.84
2 Sepal.Width 3.06
3 Petal.Length 3.76
4 Petal.Width 1.20
After I execute mean_data(iris), the imports are accessible everywhere. EVERYWHERE!
And base::use() is still broken even in the latest R versions.
Like, it is so completely broken:
use('dplyr', 'mutate')
iris |> mutate(Petal.Area = Petal.Length * Petal.Width)
#> Error in mutate(iris, Petal.Area = Petal.Length * Petal.Width) :
#> could not find function "mutate"The issue is that subsequent
library()calls for an identical package are ignored, and the same is true forbase::use(). Bananas. Completely broken.
This is noted by R core team:
This functionality is still experimental: interfaces may change in future versions.
1.2 Worst: Using require()

~Just…no. I’ll be making a hot take here that sounds controversial, but this solution is the worst thing ever existed in R to attach the packages. ~
Update: This solution may be bad, but at least not worse than base::use().
This function returns a Boolean value:
require(pkg) |>
suppressMessages() |>
suppressWarnings() |>
print()[1] FALSE
It returns TRUE if the package is successfully loaded and FALSE otherwise.
And should only be applicable inside functions to check if a package is available.
check_package = function() {
if (require(pkg, quietly = TRUE)) {
print("Package loaded successfully")
} else {
print("Package not available")
}
}
check_package()[1] "Package not available"
Using require() at the top of a script is how you get mysterious errors 50 lines later. Only acceptable inside functions when you actually check the return value.
Seriously, this is just library() where you can place it at the top level of your script, but add another extra steps.
1.3 The pro boxer: {pacman}
This guy will punch you to death. Just kidding, Manny Pacquiao is a great boxer :). The pacman package tries to streamline package management with functions like p_load().
Do you know this?
if (!require(pkg)) {
install.packages("pkg")
library(pkg)
}Well, they made a shortcut, with pacman::p_load():
pacman::p_load(pkg)You can do the same as above, except you can do this for multiple packages.
Here’s how:
pacman::p_load(pkg1, pkg2, pkg3)Sounds convenient, right?

Actually mixes two completely different responsibilities:
- Installation (one-time setup)
- Loading (analysis step)
Great for interactive playtime. Disastrous in scripts, packages, CI/CD, or any environment without internet. Also:
It violates the single responsibility principle, harder than a toddler with a drum kit.
-
This is like a pineapple pizza

1.4 The classic library()
Such a classic function, isn’t it? After all, this is the most used function to attach R package namespace. It is a standard practice that most R users use, and it is safe: It will throw an error if pkg is not installed. This function is traditional and simple:
library(pkg)That’s it, right?
I hope it was that simple, but it has some serious downsides:
- It attaches the entire package namespace to the search path,
- It can lead to namespace clash, particularly if multiple packages have functions with the same name. This can make debugging difficult and lead to unexpected behaviors in your code.
- It makes the imports unclear which functions come from which packages
- All exported functions are available, even if you only need one or two
To detach the attached package namespace in the search path, use detach() function with package : keyword:
detach(package : pkg)1.5 library() and {conflicted} package combo
How about forcing the search path to select / deselect the imports? Introducing conflicted package.

In this approach, I combine traditional library() with the conflicted package to explicitly handle namespace conflicts.
How good? For example, I prefer using dplyr::filter() over stats::filter(), but a bit later on, I want to use stats::filter() when I want to run time series. The conflicted::conflict_prefer() handles which you want to declare “winners” of conflicts.
I’ll make a scenario to make you understand:
-
I have no use with
stats::filter()because I only want to keep the data frame based on the condition usingdplyr::filter(), and I want to load the entire dplyr namespace. Here, I declaredplyr::filter()as “winner” of the conflict:library(dplyr) conflicted::conflict_prefer('filter', 'dplyr', 'stats') filter(mtcars, cyl == 8) -
Then, I stopped using
dplyr::filter()because I want to perform time series modelling with linear filtering usingstats::filter(). Re-statestats::filter()as the “winner” of the conflict:conflicted::conflict_prefer('filter', 'stats', 'dplyr') filter(1:10, rep(1, 3))
Still loads everything. Still global. Still manual work. In my standard, this is actually good, but still not enough because it never allows granular imports and import aliasing, and besides, I’ve had better.
1.6 Tedious but Explicit: The :: Operator

Before packages like box and import introduced alternative import systems to R, the :: operator was (and still is) R’s built-in way to explicitly reference functions from specific namespaces without loading entire packages.
The :: operator is the most explicit base R solution for calling package functions. It’s part of R’s namespace system and requires no external dependencies - just base R.
Here’s how:
- The syntax is
package::function(), which tells R exactly which package to pull the function from without attaching that package to your search path.
Most of us using R are definitely using this (I am reusing an example from base::use):
mean_data = function(.data) {
dplyr::summarise(
.data, across(
where(is.numeric),
\(col) mean(col, na.rm = TRUE)
)
) |>
tidyr::pivot_longer(
cols = where(is.numeric),
names_to = "Variable",
values_to = "Ave"
)
}
mean_data(iris)# A tibble: 4 × 2
Variable Ave
<chr> <dbl>
1 Sepal.Length 5.84
2 Sepal.Width 3.06
3 Petal.Length 3.76
4 Petal.Width 1.20
This is great, compared to the previous solutions, no external packages needed and works mostly in any R version. The problem is this is way too verbose and repetitive, especially with many function calls:
ggplot2::ggplot(data, ggplot2::aes(date, y)) +
ggplot2::geom_point() +
ggplot2::geom_line() +
ggplot2::theme_minimal() +
ggplot2::labs(
x = "Date (by month)",
y = "Value (in dollars)",
title = "Monthly Value in Dollar"
)Being typing-intensive is why I called this solution “tedious”.

Respectable, but I bet nobody wants to type that many in 2025.
1.7 Second to best: {import} package
It is so close!

This package is made before box. So, before box, the import package is the best solution ever made, arrived to fix library()’s most egregious issues. Created by Stefan Milton Bache (of pipe fame), it brings selective imports to R without requiring a complete paradigm shift.
The first example is simple: Normal imports with aliases.
import::from(
dplyr,
select, rename, keep_when = filter, mutate, summarise, n
)
import::from(tidyr, long = pivot_longer, wide = pivot_wider, drop_na)
import::from(ggplot2, diamonds, cut_width)
diamonds |>
keep_when(
cut %in% c("Ideal", "Premium"),
carat > 1
) |>
drop_na() |>
mutate(
price_per_carat = price / carat,
size_category = cut_width(carat, 0.5)
) |>
select(carat, cut, color, price, price_per_carat, size_category) |>
wide(
names_from = cut,
values_from = price_per_carat,
values_fn = median
) |>
summarise(
across(c(Ideal, Premium), \(col) mean(col, na.rm = TRUE)),
n = n()
)# A tibble: 1 × 3
Ideal Premium n
<dbl> <dbl> <int>
1 6494. 5978. 9951
Use backticks around %>% since it is under non-syntactic names.
import::from(dplyr, select, filter, mutate, summarise, n, relocate)
import::from(magrittr, `%>%`)
import::from(tidyr, long = pivot_longer, wide = pivot_wider, drop_na)
mtcars %>%
filter(cyl == 6) %>%
mutate(
hp_per_cyl = hp / cyl,
efficiency = mpg / disp
) %>%
select(mpg, disp, hp, hp_per_cyl, efficiency, everything()) %>%
summarise(
across(
c(mpg, hp_per_cyl, efficiency),
list(
mu = \(x) mean(x, na.rm = TRUE),
sigma = \(x) sd(x, na.rm = TRUE)
),
.names = "{.col}..{.fn}"
),
n = n()
) %>%
long(
cols = contains(c("mu", "sigma")),
names_sep = "\\..",
names_to = c("Variable", "Stat"),
values_to = "Est"
) %>%
wide(
names_from = Stat,
values_from = Est
) %>%
relocate(n, .after = last_col()) %>%
mutate(
se = sigma / sqrt(n),
cv = sigma / mu
)# A tibble: 3 × 6
Variable mu sigma n se cv
<chr> <dbl> <dbl> <int> <dbl> <dbl>
1 mpg 19.7 1.45 7 0.549 0.0736
2 hp_per_cyl 20.4 4.04 7 1.53 0.198
3 efficiency 0.112 0.0231 7 0.00872 0.206
It’s so awesome. Why?
- No masking
- Explicit at the top
- Works with roxygen2 (@importFrom)
- Imports the pipe like any other function
There’s still some limitations. Even though import provide necessities that solves my problem in R’s import system -
-
It has no unifying solution to attach the imports in the current environment. In fact, import functions still attach imported functions to the parent environment (usually global). What I mean is that they’re not truly scoped to a module or function. Thus, the use of
import::here():The expression we made,
with(iris, { ... })creates a temporary environment that disappears immediately. Socorr()is placed exactly there, inside that temporary environment, and you cannot reusecorr()somewhere in the environment, even in the global environmentcorr(1:10, 2:11) #> Error in corr(1:10, 2:11) : could not find function "corr"This is better than loading entire packages, but not as clean as lexical scoping.
The package was designed primarily for CRAN packages. File-based modules feel like an afterthought rather than a first-class feature.
It lacks support for nested module hierarchies. You can import from files with this package, but you can’t organize modules into sophisticated directory structures with their own internal dependencies.
Unlike box, there’s no way to import a whole package as an object without attaching names
1.8 The ergonomically superior {box} package

Finally, some good food.
In 2021, Konrad Rudolph looked at R’s prehistoric import system, said:
“This is rubbish”
Disclaimer: I don’t know if he said it, I said this just for fun ;)
I strongly agree. And then dropped one of the magnum opus: box — he dropped it like Gordon Ramsay dropping a perfectly seared Wellington on the pass.
This isn’t “slightly nicer imports” — it’s a complete rethinking of how R package should be loaded, and R code should be organized and namespaced. It brings true module systems (like Python, JavaScript, or Ruby) to R.
There’s 4 of a kind to import like a sane person:
box::use(
purrr, # 1
tbl = tibble, # 2
dplyr = dplyr[filter, select], # 3
stats[st_filter = filter, ...] # 4
)Source: https://github.com/klmr/box
-
Attached the names? Nah, even better:
- Imports the entire purrr package as an object.
- Nothing goes into the search path.
- You use it as
purrr$map(),purrr$keep(), etc.
- Zero risk of masking, zero pollution. Pure bliss.
- Imports the entire purrr package as an object.
-
Whole package? But make it short
- Same vibe as 1, but you give the package a cute little nickname.
- Now you write
tbl$tbl_df(),tbl$as_tibble(), etc.
- Perfect when you hate typing
tibble::but also hate global mess.
-
I want the whole namespace… but only some names in my face.
A killer move, actually: import the whole package as an object, and selectively attach only the functions you actually want to write naked.
So your pipelines stay clean:
filter(),select(),mutate()— all smooth, drama-free.-
But when you need the weird stuff, you still have the entire namespace sitting there like:
dplyr$reconstruct_tibble_from_who_knows_what()
-
“I refuse to be gaslit by
stats::filter()ever again.”- “I want everything from
{stats}(because base R is already everywhere), butstats::filter()is a war criminal that keeps fighting withdplyr::filter().” - So basically, everything from
{stats}is attached, but rename that one cursed function tost_filter()so it never bites me again. - The
...means “everything else, with their original names”.
- “I want everything from
But wait, there’s more!
Here, watch the madness of how I apply box to load package deps:
box::use(
dplyr[
select, rename,
keep_when = filter, # rename because we want to avoid needless fighting
mutate, summarise,
across, everything
],
tidyr[pivot_longer, pivot_wider, drop_na],
magrittr[`%>%`], # yes, don't forget that the pipe is just another import
ggplot2[ggplot, aes, geom_point, theme_minimal, labs, ggsave],
lubridate[ymd, year, month, floor_date],
data.table[fread] # because sometimes you need speed, not dignity
)Less :: spam. No package::function() that makes your code look like it’s been hit by shrapnel. Zero library() / require().
And then, the part that makes grown R programmers cry tears of joy — You are also allowed to reuse exported namespace from an R script or a folder as a module.
box::use(
app/models/glm_fit[...], # brings everything exported
app/plots/theme_pub[theme_pub], # only the theme
app/utils/cleaning[clean_names, fix_dates],
./secret_sauce # local folder / script = module
)With box, you can create modules that encapsulate your code and its dependencies — another revolutionary and W move in R community. This package is making my life easier in managing and reuse code across different projects.
This approach aligns well with modern programming practices and helps to keep your codebase clean and maintainable.
1.8.1 Little resources
Other resources to learn more about this package:
2 Remarks
While there are multiple ways to load packages in R, not all methods are created equal — some were created more equal than others, and some were created as war crimes. The choice of method can significantly impact the readability, maintainability, and reliability of your code, as well as your blood pressure and willingness to live.
I strongly recommend using the box package in your projects, personal or not, for its modular approach and ability to avoid namespace clashes, making it a superior choice for loading packages in R. Like, come on, it’s 2025. We have smartphones that can detect if you’re about to have a heart attack. We have AI that can write poetry. We should NOT still be debugging namespace conflicts like it’s 2005.