Lists and Environments in R

· 10 min read · beginner
lists environments r-fundamentals data-structures
Part 6 of the r-fundamentals series

Lists and environments are fundamental data structures in R that you’ll encounter frequently as you progress beyond beginner tutorials. While vectors hold values of the same type, lists can hold objects of different types—and environments provide the namespace system that powers R’s scoping rules.

Why Use Lists?

A list is a vector of objects. Unlike atomic vectors, which require all elements to be the same type, lists can contain anything: numbers, strings, other lists, functions, or even data frames. This flexibility makes lists the go-to structure for complex data in R.

Creating Lists

Use the list() function to create a list:

# A simple list with mixed types
my_list <- list(
  name = "Alice",
  age = 30,
  scores = c(85, 92, 78),
  active = TRUE
)

# Access the first element
my_list[[1]]
# [1] "Alice"

# Access by name
my_list$name
# [1] "Alice"

# Access using double brackets
my_list[["age"]]
# [1] 30

List Structure and Manipulation

Lists have a tree-like structure. Use str() to inspect any list:

str(my_list)
# List of 4
#  $ name : chr "Alice"
#  $ age  : num 30
#  $ scores: num [1:3] 85 92 78
#  $ active: logi TRUE

Common list operations:

# Add a new element
my_list$city <- "London"

# Remove an element
my_list$age <- NULL

# Number of top-level elements
length(my_list)
# [1] 4

# Get element names
names(my_list)
# [1] "name"    "scores"  "active"  "city"

Applying Functions to Lists

The lapply() function applies a function to each element and returns a list. The related sapply() tries to simplify the result:

# lapply returns a list
result <- lapply(my_list$scores, function(x) x * 2)
str(result)
# List of 3
#  $ : num 170
#  $ : num 184
#  $ : num 156

# sapply returns a vector
result <- sapply(my_list$scores, function(x) x * 2)
result
# [1] 170 184 156

The purrr package provides more consistent alternatives (map(), map_dbl(), etc.) that are part of the tidyverse.

Understanding R Environments

An environment is a collection of symbol-value pairs (a namespace). Every environment has a parent environment, creating a chain that R searches when you reference a name.

The Global Environment

When you work in R interactively, you’re primarily using the global environment:

# Create variables in the global environment
x <- 10
y <- 20

# Check the global environment
ls()
# [1] "x" "y"

# See the environment of a function
environment()
# <environment: R_GlobalEnv>

How Scoping Works

R uses lexical scoping, meaning it looks for variables based on where functions are defined, not where they’re called:

add <- function(a, b) {
  a + b
}

add(2, 3)
# [1] 5

This seems simple, but the real power appears with closures—functions that capture their enclosing environment:

make_counter <- function() {
  count <- 0
  function() {
    count <<- count + 1
    count
  }
}

counter1 <- make_counter()
counter2 <- make_counter()

counter1()
# [1] 1

counter1()
# [1] 2

counter2()
# [1] 1

Each counter has its own captured environment with its own count variable.

Accessing Environment Contents

# List all objects in an environment
ls(envir = globalenv())

# Get the value of a symbol
get("x", envir = globalenv())

# Check if a name exists
exists("x", envir = globalenv())

# Remove a variable
rm("x", envir = globalenv())

Package Environments

When you load a package with library(), its namespace becomes part of the search path:

search()
# [1] ".GlobalEnv"        "package:stats"     
# [3] "package:graphics"  "package:grDevices"
# [4] "package:utils"     "package:datasets" 
# [5] "package:base"

R searches these environments in order when resolving function names. The :: operator lets you explicitly reference a specific namespace:

# Explicitly use stats::filter, not dplyr::filter
stats::filter

Common Pitfalls

1. Unintended List Coercion

When using c() with different types, R coerces everything to the most flexible type:

c(1, "a")
# [1] "1" "a"  # Both become character

Lists avoid this issue:

list(1, "a")
# [[1]]
# [1] 1
# 
# [[2]]
# [1] "a"

2. Forgetting [[ ]]

Single brackets [ ] subset a list, returning a sub-list. Double brackets [[ ]] extract the actual element:

my_list[1]       # Returns list($name = "Alice")
my_list[[1]]     # Returns "Alice"

3. Modifying Global Variables by Accident

Using <- inside a function only modifies local copies. Use <<- or assign to a specific environment:

counter <- function() {
  total <<- total + 1  # Modifies global total
  total
}

total <- 0
counter()
# [1] 1
total
# [1] 1

Be cautious with <<-—it can make code hard to debug.

Practical Applications

Lists are ideal for:

  • Returning multiple values from a function
  • Storing heterogeneous data (like survey responses)
  • Building hierarchical data structures
  • Working with JSON data

Environments power:

  • Package namespaces (isolation between packages)
  • Function closures and factory functions
  • Memoization and caching
  • Object-oriented programming in R (S3, S4, R6)

Conclusion

Lists and environments are essential tools in R programming. Lists give you flexibility in data structure, while environments provide the namespace system that makes R’s scoping work. Master these concepts, and you’ll understand why R behaves the way it does—and how to make it do what you want.

As you continue learning, you’ll see lists in action when working with model outputs, JSON data, and statistical results. Environments become relevant whenever you write functions that need to “remember” state or when you’re debugging why a variable has a particular value.