R Environments Explained: Global, Local, Package, Execution & Empty
An environment in R is a bag of names. Every name points to a value, and every environment has a parent. Understanding environments unlocks scoping, closures, namespaces, and debugging — the machinery behind how R finds your variables.
Most R users never think about environments. You type x <- 5 and x exists. You call a function from a package and it just works. But behind every variable lookup, R is walking a chain of environments. Once you see that chain, R's behavior stops being magical and starts being predictable.
Introduction
An environment is a data structure with two parts:
A frame — a set of name-value bindings (like a named list)
A parent — a reference to another environment (except emptyenv())
Every time you create a variable, it goes into an environment. Every time R looks up a variable, it searches environments in order. This tutorial covers:
The four special environments: global, base, empty, package
How to create environments and add bindings
The parent chain and search path
Inspecting environments with ls(), get(), exists()
Execution environments (created every time a function runs)
The Global Environment
The global environment is your interactive workspace — where variables you create at the console live:
# Your workspace IS the global environment
x <- 42
y <- "hello"
# globalenv() returns it
cat("Global env:", environmentName(globalenv()), "\n")
# .GlobalEnv is an alias
cat("Same thing:", identical(globalenv(), .GlobalEnv), "\n")
# List what's in it
cat("Objects in global env:", paste(ls(globalenv()), collapse = ", "), "\n")
When you type x at the console, R first looks in the global environment. If it finds x there, it returns the value. If not, it looks in the parent of the global environment, and so on.
The Environment Hierarchy
R environments form a chain through their parents. The search path goes:
Global env -> Package envs (in order of library() calls) -> Base env -> Empty env
# Walk the parent chain from the global environment
env <- globalenv()
chain <- character()
while (!identical(env, emptyenv())) {
chain <- c(chain, environmentName(env))
env <- parent.env(env)
}
chain <- c(chain, "emptyenv")
cat("Search path:\n")
for (i in seq_along(chain)) {
cat(sprintf(" %d. %s\n", i, chain[i]))
}
The empty environment (emptyenv()) is the ancestor of all environments. It has no parent and no bindings. It's the end of the search chain.
The base environment (baseenv()) contains base R functions like c(), mean(), print(). Its parent is the empty environment.
# The three fundamental environments
cat("Global:", environmentName(globalenv()), "\n")
cat("Base:", environmentName(baseenv()), "\n")
cat("Empty:", environmentName(emptyenv()), "\n")
# Base env's parent is the empty env
cat("Base parent is empty:", identical(parent.env(baseenv()), emptyenv()), "\n")
# search() shows the full path as a character vector
cat("\nsearch() path:\n")
print(search())
Creating Environments
You can create new environments with new.env():
# Create a new environment
e <- new.env(parent = emptyenv())
# Add bindings
e$name <- "Alice"
e$age <- 30
e$scores <- c(95, 88, 92)
# List bindings
cat("Bindings in e:", paste(ls(e), collapse = ", "), "\n")
cat("Name:", e$name, "\n")
cat("Age:", e$age, "\n")
cat("Scores:", e$scores, "\n")
Environments have reference semantics — unlike lists, modifying an environment in a function changes the original:
This is why environments are useful for mutable state — they behave like references, not values.
Inspecting Environments: ls(), get(), exists()
Three functions let you inspect what's inside an environment:
# Create a populated environment
env <- new.env(parent = emptyenv())
env$x <- 1:10
env$name <- "test"
env$flag <- TRUE
# ls() — list all names
cat("Names:", paste(ls(env), collapse = ", "), "\n")
# ls.str() — list names with structure
ls.str(env)
# get() — retrieve a value by name
env <- new.env(parent = emptyenv())
env$pi_approx <- 3.14159
val <- get("pi_approx", envir = env)
cat("Got:", val, "\n")
# exists() — check if a name is bound
cat("pi_approx exists:", exists("pi_approx", envir = env), "\n")
cat("missing exists:", exists("missing", envir = env), "\n")
# inherits = FALSE means "only look in THIS env, not parents"
cat("mean exists here:", exists("mean", envir = env, inherits = FALSE), "\n")
cat("mean exists in chain:", exists("mean", envir = env, inherits = TRUE), "\n")
The inherits argument is key. By default, get() and exists() search up the parent chain — just like R's normal variable lookup. Set inherits = FALSE to look only in the specified environment.
Execution Environments
Every time you call a function, R creates a fresh execution environment for that call. Local variables live there:
# Each function call gets its own environment
show_env <- function() {
local_var <- "I'm local"
cat("Execution env:", format(environment()), "\n")
cat("Parent:", environmentName(parent.env(environment())), "\n")
cat("local_var:", local_var, "\n")
}
# Two calls = two different environments
show_env()
show_env()
# local_var doesn't exist outside
cat("local_var exists globally:", exists("local_var", envir = globalenv()), "\n")
The execution environment's parent is the enclosing environment — the environment where the function was defined, not where it was called. This is the basis of lexical scoping.
# Function arguments also live in the execution environment
inspect <- function(a, b) {
cat("Arguments in this env:\n")
cat(" a =", a, "\n")
cat(" b =", b, "\n")
cat(" all names:", paste(ls(), collapse = ", "), "\n")
}
inspect(10, 20)
Environments as Data Structures
Because environments have reference semantics and O(1) name lookup (they use hash tables), they're sometimes used as data structures:
# Exercise: Write a function parent_chain(env) that returns
# a character vector of all environment names from env
# up to emptyenv().
# Test it with: parent_chain(globalenv())
# Write your code below:
Click to reveal solution
```r
parent_chain <- function(env) {
chain <- character()
while (!identical(env, emptyenv())) {
nm <- environmentName(env)
if (nm == "") nm <- format(env)
chain <- c(chain, nm)
env <- parent.env(env)
}
chain <- c(chain, "emptyenv")
chain
}
result <- parent_chain(globalenv())
cat("Parent chain:\n")
for (i in seq_along(result)) {
cat(sprintf(" %d. %s\n", i, result[i]))
}
**Explanation:** We walk up the chain using `parent.env()` until we hit `emptyenv()`. `environmentName()` returns "" for anonymous environments, so we fall back to `format()` which shows the memory address.
Exercise 2: Environment as Counter
# Exercise: Create a counter using an environment.
# make_counter() should return a list with:
# $increment() — adds 1 to the count
# $get() — returns current count
# $reset() — sets count back to 0
# The count should persist between calls.
# Write your code below:
**Explanation:** The environment `env` is shared by all three functions because they were defined in the same scope. Each function can read and modify `env$count`, and the changes persist because environments have reference semantics.
Summary
Concept
Function
Description
Global env
globalenv(), .GlobalEnv
Your interactive workspace
Base env
baseenv()
Contains base R functions
Empty env
emptyenv()
Top of the chain, no parent
Create env
new.env(parent = ...)
Make a new environment
Parent
parent.env(env)
Get the parent environment
List names
ls(env)
Show all bindings in an environment
Get value
get("x", envir = env)
Retrieve a value by name
Check existence
exists("x", envir = env)
Test if a name is bound
Search path
search()
Show the full environment chain
Env name
environmentName(env)
Get the name of an environment
Key takeaways:
Environments form a chain through their parents
Variable lookup walks this chain from current to emptyenv()
Environments have reference semantics (no copy-on-modify)
Every function call creates a new execution environment
FAQ
How is an environment different from a list?
Two key differences: (1) Environments have reference semantics — modifying an environment in a function changes the original. Lists use copy-on-modify. (2) Environments have a parent — failed lookups continue to the parent. Lists don't chain.
What happens if R reaches emptyenv() without finding a variable?
R throws the error object 'x' not found. The empty environment has no bindings and no parent, so the search stops there.
Can I change the parent of an environment?
Yes, with parent.env(env) <- new_parent. But this is rarely needed and can cause confusing behavior. It's mainly used internally by R's package system.
What's Next?
Environments are the foundation for understanding how R finds variables. Continue with:
Lexical Scoping in R — how R uses the environment chain to look up variable names
R Closures — functions that capture and remember their defining environment
R Namespaces — how packages use environments to export functions and prevent conflicts