rguides

with

with(data, expr, ...)

The with() function evaluates an R expression inside a local environment constructed from a data frame, list, or environment. Names in data are looked up before anything in the caller’s environment, which lets you refer to them by short name instead of writing df$col over and over. It is one of the oldest pieces of non-standard evaluation in base R and ships in every supported version.

Signature and arguments

with(data, expr, ...)
  • data — an environment, list, data frame, or integer used by sys.call()-style helpers. Its names become local bindings inside the constructed environment.
  • expr — the expression to evaluate. Use a { ... } block when you need more than one statement, since ; only binds the first piece.
  • ... — present for method extensions; the default method does not consume it.

with() returns the value of expr and never modifies data. There is also a default method plus methods for data.frame and array; the list method behaves the same way as the default.

How it works

with() builds a fresh environment whose parent is the caller’s environment. The expression is evaluated inside that constructed environment, so names in data are found first. Anything in the caller’s environment, including functions and global objects, stays reachable through the parent. When the call returns, the constructed environment is discarded along with any assignments made inside it.

That is why a line like with(df, x <- x + 1) does not change df. The new x is created in the temporary environment and thrown away when with() returns. If you want the change to stick, reach for within() instead.

Basic examples

Run a summary inside a data frame:

with(mtcars, mean(mpg))
#> [1] 20.09062

The same call written out by hand returns the identical value, but it makes the data source explicit and is the form you want in package code or anywhere a reader is not already staring at mtcars:

mean(mtcars$mpg)
#> [1] 20.09062

The two forms are equivalent for atomic columns. The with() form is shorter; the explicit form does not depend on lookup order and is easier to grep for.

Filter rows without typing the data frame name twice:

with(airquality, Ozone[Temp > 80])
#> [1] 39 36 44 45 36 44 46 30 ...

This is the canonical “with” pattern: build a logical or numeric condition, then index. The same code without with() would repeat airquality$ in two places and is easy to mistype.

Run a compound model on a fresh data frame without ever naming the columns:

with(
  data.frame(
    u    = c(5, 10, 15, 20, 30, 40, 60, 80, 100),
    lot1 = c(118, 58, 42, 35, 27, 25, 21, 19, 18),
    lot2 = c(69, 35, 26, 21, 18, 16, 13, 12, 12)
  ),
  list(
    summary(glm(lot1 ~ log(u), family = Gamma)),
    summary(glm(lot2 ~ log(u), family = Gamma))
  )
)

The columns u, lot1, and lot2 are reachable inside the model formulas because the data frame is the constructed environment. The example is lifted straight from the help page and shows the pattern at its purest: an anonymous data frame drives an anonymous analysis.

Using a block expression

For more than one statement, wrap the body in { ... }. Without braces, with() only sees the first expression because of how ; binds:

with(mtcars, {
  v8 <- cyl == 8
  sum(v8)
})
#> [1] 15

Inside the block, every new binding lives only in the temporary environment. After the call, v8 is gone from your workspace. If you want the helper to survive, assign the whole call: flag <- with(mtcars, cyl == 8).

A common pattern is to drive a plot straight from the columns of a data set, where the boxplot formula len ~ dose reads naturally because both names resolve in the constructed environment:

with(ToothGrowth, {
  boxplot(
    len ~ dose, boxwex = 0.25, at = 1:3 - 0.2,
    subset = (supp == "VC"), col = "yellow",
    main = "Guinea Pigs' Tooth Growth",
    xlab = "Vitamin C dose mg", ylab = "tooth length",
    ylim = c(0, 35)
  )
})

For modeling and graphics, prefer the data argument of the function when it has one. boxplot(len ~ dose, data = ToothGrowth, ...) is the same plot with the source of len and dose right there in the call.

Creating modified copies with within()

within() shares the same shape but inspects the local environment after evaluating expr and copies any new or changed bindings back into data. It is the right tool when you want to derive new columns and keep them:

aq <- within(airquality, {
  lOzone <- log(Ozone)
  Month  <- factor(month.abb[Month])
  cTemp  <- round((Temp - 32) * 5 / 9, 1)
  S.cT   <- Solar.R / cTemp
  rm(Day, Temp)
})
head(aq)

within() is the safer replacement for the older transform(). It works on any data frame, evaluates each new binding sequentially, and lets you rm() columns you no longer need. The result is a fresh data frame; the input is left alone.

For lists, the list method adds a keepAttrs argument. Setting it to FALSE skips the input’s attributes and name reordering, which is faster when you do not need either:

lst <- list(a = 1, b = 2)
within(lst, { c <- a + b })
#> $a
#> [1] 1
#> $b
#> [1] 2
#> $c
#> [1] 3

When with() shines, and when it does not

Reach for with() for short interactive sessions, plotting, and one-off summaries where the data is in scope and the expression is brief. The official R help page says it plainly: with() is meant for interactive use, and using it inside a function or library can silently shadow local variables in the caller.

For production code, prefer the data argument of modeling and graphics functions, or write df$col explicitly. Both make the source of every name obvious, and neither relies on the constructed-environment trick. attach() is a related but older alternative that puts the data frame on the search path; with() is almost always a better choice because the binding only exists for the duration of the call.

Common gotchas

  1. <- does not mutate the data. Use within() if you want the change to stick.
  2. Masking can bite you. If data has a column named x and your caller also has an x, the column wins.
  3. Single-expression parsing. with(data, a <- 1; a + 1) is parsed as a + 1 because of operator precedence; reach for { ... } whenever the body has more than one statement.
  4. Data frames only take atomic columns, so within(df, tmp <- list(...)) will fail.
  5. with() does not replace the data argument of lm, glm, boxplot, and friends. Wrap a call in with() only for short scripts; use the data argument for anything that ships.

For programming, prefer explicit df$col references or pass data to the modeling function. The tidyverse equivalent of within() for column derivation is dplyr::mutate().

See also

  • subset() — filter a data frame by a condition.
  • data.frame — the most common data argument.
  • list — using a list as data, including the keepAttrs argument.