rguides

environment()

An environment in R is a data structure that associates (binds) names to objects. Unlike lists, environments have a parent environment that forms a scope chain, when R looks up a name, it searches the current environment first, then walks up the parent chain until it finds the name or reaches the empty environment. This is how R’s lexical scoping works.

Creating environments

You can create new environments with new.env(). By default, the parent is the current environment (the global environment in interactive use).

# Create a new empty environment
env <- new.env()

# Add variables to it using $ or [[]]
env$x <- 10
env[["y"]] <- "hello"

# Access variables
env$x
# [1] 10

Once you have an environment populated, the standard accessor functions let you inspect and modify bindings without writing custom lookup logic. ls() lists all bound names, exists() checks for presence, and get() and assign() accept character-string names, which is useful when names are computed at runtime rather than hard-coded. These functions work across any environment on the search path.

Working with environments

FunctionDescription
ls(env)List all names in an environment
get("x", envir = env)Retrieve a value by name
assign("x", value, envir = env)Set a value by name
exists("x", envir = env)Check if a name exists
rm("x", envir = env)Remove a binding
parent.env(env)Get the parent environment
environmentName(env)Get the environment’s name
env <- new.env()
env$a <- 1:5
env$b <- "data"

ls(env)
# [1] "a" "b"

exists("a", envir = env)
# [1] TRUE

get("b", envir = env)
# [1] "data"

The parent-child relationship between environments is what drives scoping. When you ask for a name in a child environment that does not contain it, R automatically checks the parent, then the parent’s parent, continuing until it finds the name or reaches the empty environment. This chain is why package functions can access base R functions without explicit imports, and why variables defined in the global environment are visible inside package code.

The scope chain

Every environment has a parent. When you use a name, R searches up the chain:

# Create parent environment
parent <- new.env()
parent$shared <- "I'm in parent"

# Create child environment with parent
child <- new.env(parent = parent)
child$local <- "I'm in child"

child$shared
# [1] "I'm in parent"

child$local
# [1] "I'm in child"

Environments are not simply a low-level detail of scoping. R uses them throughout its own infrastructure: every package you attach with library() gets an environment placed on the search path, and every function you define captures its enclosing environment as a permanent binding. Understanding this dual role — scoping mechanism and namespace container — clarifies why environments show up in debugging, package development, and metaprogramming contexts.

Common patterns

Package namespaces

Packages use environments to isolate their code. When you load a package with library(), R attaches it to the search path:

# The base environment is at the top of the search path
search()
# [1] ".GlobalEnv"        "package:stats"     
# [3] "package:base"      

# dplyr creates its own environment with its functions
library(dplyr)
ls(where = as.environment("package:dplyr"))[1:5]
# [1] "%>%"       "filter"    "select"    "mutate"    "arrange"

The function-environment connection is the mechanism behind closures in R. When a function is created inside another function, it captures the enclosing environment, which persists after the outer function returns. This is how factory functions preserve state across calls — the returned function carries a snapshot of the variables that existed when it was defined, stored in its environment attribute.

Function environments

Every function has an associated environment where its free variables are looked up:

make_adder <- function(x) {
  function(y) x + y
}

add5 <- make_adder(5)
environment(add5)
# <environment: 0x558a2b6e8>

# x is stored in the function's enclosing environment
get("x", envir = environment(add5))
# [1] 5

Environments also work well as mutable key-value stores outside the scoping context. Because they use reference semantics — two variables pointing to the same environment see each other’s changes — they fit caching, shared configuration, and registry patterns. Lookups are O(1) via an internal hash table, which makes them faster than named lists for large collections where you need frequent name-based access.

Caching with environments

Environments are mutable and use reference semantics, making them useful for caching:

cache <- new.env()
compute_expensive <- function(key) {
  if (exists(key, envir = cache)) {
    message("Fetching from cache")
    return(get(key, envir = cache))
  }
  result <- Sys.sleep(1) # simulate expensive computation
  result <- paste0("computed: ", key)
  assign(key, result, envir = cache)
  result
}

compute_expensive("data1")  # Takes ~1 second
# [1] "computed: data1"
compute_expensive("data1")  # Instant (from cache)
# Fetching from cache
# [1] "computed: data1"

When to use environments directly

Most R code never manipulates environments explicitly, variables, functions, and package imports handle scoping transparently. You interact with environments directly when you need mutable shared state (the cache pattern above), when building tools that inspect or modify the calling frame (used in packages like rlang and shiny), or when you want hash-map-style fast lookup by name. Environments look up names in O(1) time via a hash table, unlike lists which scan linearly. For a large collection of named objects where lookup speed matters, an environment beats a named list.

Environments in R are reference objects — unlike vectors, they are not copied when modified. Two variables can point to the same environment, and a change through one is visible through the other. The global environment (.GlobalEnv) is where interactive assignments land. Each function call creates a new environment whose parent is the function’s enclosing environment, forming the search chain for name lookup.

See also