rguides

within

within(data, expr, ...)

within() runs an R expression inside a local environment built from a data frame or list, then returns a copy of that input with the expression’s bindings copied back in. It is the write counterpart of with(): with() lets you read columns without typing df$col everywhere, and within() lets you add, modify, or remove columns the same way.

The function was added in R 2.13.0 (April 2011) and lives in the base package, so it loads with every R session. It is an S3 generic with within.data.frame and within.list methods, and the data-frame method is what you will reach in practice.

Signature

within(data, expr, ...)

The data-frame method takes only the first two arguments plus ..., so the generic signature already covers almost every call you will write. The list method adds one extra argument, keepAttrs, which controls whether the result preserves the input’s attributes and the original name order. Setting keepAttrs = FALSE skips a small amount of bookkeeping when neither matters, which is useful in tight loops over throwaway lists.

within(data, expr, ..., keepAttrs = TRUE)
ArgumentTypeDefaultMeaning
datalist or data framerequiredThe object to copy and modify. The original is never mutated.
exprexpression (typically a { ... } block)requiredCode evaluated in a local environment seeded from data.
keepAttrslogicalTRUEList method only. When TRUE, the result preserves the input’s attributes and the original name order. FALSE is a small speed-up when neither matters.
...further argsReserved for method extensions. The data-frame method ignores it.

Return value

A modified copy of data, with the same class as the input. A data frame stays a data frame, a list stays a list. The original data is never touched, so you must assign the result back if you want to keep it:

aq <- within(airquality, lOzone <- log(Ozone))
head(aq, 3)
#   Ozone Solar.R Wind Temp Month Day     lOzone
# 1    41     190  7.4   67     5   1  3.7135721
# 2    36     118  8.0   72     6   2  3.5835189
# 3    12     149 12.6   74     7   3  2.4849066

If expr creates a binding that data cannot store (for example, a list inside a data frame), the call fails after evaluation with a [<-.data.frame error, not with a tidy early diagnostic.

Adding and modifying columns

The whole point is that column names in expr resolve to the columns of data directly, so the code reads like a small recipe instead of a chain of df$col calls:

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, 3)
#   Ozone Solar.R Wind Month cTemp     S.cT
# 1    41     190  7.4   May   5.6 33.92857
# 2    36     118  8.0   Jun  14.4  8.19444
# 3    12     149 12.6   Jul  23.3  6.39485

Notice that S.cT uses cTemp, the column defined moments earlier in the same block. within() evaluates the expression sequentially, so later bindings can reference earlier ones. The older transform() evaluates each argument against the original data, which means a new column cannot depend on another new column in the same call. within() is what the manual recommends when you need that.

Removing columns with rm()

Any name dropped via rm() inside the block is removed from the result. This is the second thing transform() cannot do:

within(iris, rm(Species))[1:2, ]
#   Sepal.Length Sepal.Width Petal.Length Petal.Width
# 1          5.1         3.5          1.4         0.2
# 2          4.9         3.0          1.4         0.2

iris itself is untouched because within() always returns a copy.

The list method and keepAttrs

On a list, the result keeps the original name order and attributes by default:

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

The output keeps a and b first because they came from data, then appends the new binding c. Any attributes on lst are preserved by default, which matters when the input carries names or metadata you want to keep visible. If neither attributes nor order matter, for example when you are assembling a throwaway list inside a function, pass keepAttrs = FALSE for a small speed-up:

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

The data-frame method does not accept keepAttrs. Passing it on a data frame silently swallows the argument into ... and ignores it.

Failure modes

Two failure modes catch people out often enough to be worth seeing once. The first is a NULL binding, which within() silently drops from the result:

within(iris, { bad <- NULL; ok <- Sepal.Length * 2 })[1:2, ]
#   Sepal.Length Sepal.Width Petal.Length Petal.Width  ok
# 1          5.1         3.5          1.4         0.2 10.2
# 2          4.9         3.0          1.4         0.2  9.8

Notice that bad never appears in the output, even though the block assigned it. If you want a real placeholder column of missing values, write bad <- NA instead. The second is a type mismatch on a data frame, which only fails when R tries the subassignment:

within(iris, weird <- list(1, 2))
# Error in `[<-.data.frame`(`*tmp*`, "weird", value = list(1, 2)) :
#   replacement has 2 rows, data has 150

The error message points at *tmp* and quotes only the row count, so it takes a moment to trace the failure back to the weird <- list(1, 2) line. Wrapping a value in list() is rarely what you want inside a data frame; flatten it to a vector of the right length first.

Common pitfalls

A few sharp edges show up regularly:

  • Always returns a copy. Forgetting to assign the result back is the most common mistake. within(df, x <- 1) on its own does nothing visible; the new column lives only in a transient frame.
  • NULL bindings are dropped. within() filters out any binding that evaluates to NULL, so bad <- NULL is not a way to add a placeholder column. Use NA instead if you need a real column of missing values.
  • No early type-check on data frames. Storing a list, environment, or other non-atomic object in a column fails only when R tries the subassignment. The [<-.data.frame error fires after expr has finished, with no early check on what expr produced.
  • Brace blocks matter. within(df, a <- 1; a + 1) parses the second statement as the value of expr, so the a <- 1 assignment is dropped. Use { a <- 1; a + 1 } or write multiple lines.
  • Caller environment is the parent. Local variables in your function shadow data columns only when the column name does not exist on data; the column wins otherwise. The ?within help page recommends the data argument of modeling functions over with()/within() for this reason in production code.

See also

  • with() — the read-only sibling, evaluated for side effects but returning the value of the expression.
  • subset() — picking rows and columns with a similar data, expr shape.
  • dplyr::mutate() — the tidyverse equivalent for adding or modifying columns, with sequential dependencies handled the same way.