rguides

list()

Lists are the most flexible data structure in R. Unlike vectors, which must contain elements of the same type, lists can hold objects of different types, different lengths, and even other lists or functions. This makes lists ideal for storing complex, hierarchical data and returning multiple values from functions.

Creating lists

Create a list using the list() function:

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

# Accessing elements
my_list[[1]]        # "Alice" (by index)
my_list$name       # "Alice" (by name)
my_list[["age"]]   # 30 (by name with brackets)

Lists can contain other lists, creating arbitrary tree structures. This recursive property distinguishes them from vectors and data frames: a list element can itself be a list, and that nested list can contain further lists, matrices, or functions. Use str() to inspect the full hierarchy in a compact, indented format that shows types and dimensions at each level.

List structure and inspection

Lists have a recursive nature, they can contain other lists:

# Nested list
nested <- list(
  a = 1:3,
  b = list(
    c = c("x", "y"),
    d = matrix(1:4, nrow = 2)
  )
)

# Check structure
str(nested)
# List of 2
#  $ a: int [1:3] 1 2 3
#  $ b:List of 2
#   ..$ c: chr [1:2] "x" "y"
#   ..$ d: int [1:2, 1:2] 1 2 3 4

The distinction between [ and [[ is central to working with lists correctly. Single brackets subset the list, returning another list. Double brackets extract a single element, returning the element’s own type. Getting this wrong is the most frequent list bug: list[1] gives a wrapper list, not the value, so downstream operations that expect a vector or scalar fail silently or produce confusing results.

Accessing and modifying elements

Use [[]] for single elements and [] for sub-lists:

x <- list(a = 1, b = 2, c = 3)

# Double brackets return the element itself
x[["a"]]        # 1 (numeric)
x[[1]]          # 1 (same thing)

# Single brackets return a sub-list
x["a"]          # List of 1

# Adding new elements
x$d <- 4
x[["e"]] <- 5

# Removing elements
x[["b"]] <- NULL

Lists and vectors convert in both directions, but the semantics differ. unlist() recursively flattens a list into a single atomic vector, concatenating names with a delimiter. as.list() wraps each element of a vector into its own list slot. For deeply nested lists, unlist() with recursive = FALSE flattens only one level, giving you control over how much structure to collapse.

Converting between lists and vectors

Use unlist() to flatten a list into a vector:

my_list <- list(a = c(1, 2), b = c(3, 4))
unlisted <- unlist(my_list)
unlisted
# a1 a2 b1 b2 
#  1  2  3  4

The reverse direction — converting a vector to a list — wraps each element individually. This is less common than unlist() but useful when you need to pass scalar arguments to a function that expects list input, or when you want to iterate over vector elements with lapply() while preserving names as list names.

Convert a vector to a list with as.list():

vec <- 1:3
as.list(vec)
# [[1]]
# [1] 1
# [[2]]
# [1] 2
# [[3]]
# [1] 3

Lists pair naturally with functional programming. lapply() applies any function to each element and collects the results back into a list. sapply() tries to simplify the output to a vector or matrix when every result has the same shape. For more control over the output type — always a double, always a character vector — use vapply() with a template value that specifies the expected return format.

Applying functions to lists

The lapply() function applies a function to each element, returning a list:

my_list <- list(a = 1:3, b = 4:6, c = 7:9)

lapply(my_list, sum)
# $a
# [1] 6
# $b
# [1] 15
# $c
# [1] 24

# sapply simplifies the result to a vector
sapply(my_list, sum)
#   a   b   c 
#   6  15  24

A practical use of lists is returning several computed values from a single function call. R functions can only return one object, so bundling a mean, median, standard deviation, and count into a named list is the standard pattern. The caller accesses each result by name, and the function signature does not need to allocate output variables or use side effects.

For more complex iteration, see the purrr functions reference.

Common patterns

Returning multiple values from a function

Lists are the standard way to return multiple objects:

calculate_stats <- function(x) {
  list(
    mean = mean(x),
    median = median(x),
    sd = sd(x),
    n = length(x)
  )
}

result <- calculate_stats(c(1, 2, 3, 4, 5))
result$mean      # 3
result$median    # 3

Lists also shine as lightweight data containers for ad-hoc records. Instead of defining a formal S3 class or creating a one-row data frame, a named list can bundle a person’s name, age, skills, and contact details in a single object. The structure is self-documenting through its names, and you can nest sub-lists for related groups of fields without any schema overhead.

Storing heterogeneous data

Lists excel when you need to combine different data types:

person <- list(
  name = "Bob",
  age = 28,
  skills = c("R", "Python", "SQL"),
  contact = list(
    email = "bob@example.com",
    phone = "555-0123"
  )
)

Lists vs other data structures

Use a list when your data is inherently heterogeneous, different types, different lengths, or a hierarchy. API responses, model outputs, and configuration objects are naturally list-shaped. Data frames are themselves lists under the hood (each column is a list element), but they enforce equal-length columns and provide a tabular interface.

For same-type, same-length collections, vectors are faster and use less memory than lists. lapply() and sapply() iterate over list elements; do.call() passes a list as function arguments. When you need to combine a list of data frames into one, use do.call(rbind, list_of_dfs) or dplyr::bind_rows(list_of_dfs).

A common mistake is confusing [ and [[ for list access. list[1] returns a list of length 1; list[[1]] returns the element itself. Use [[ whenever you want the actual value, not a wrapper list.

Lists in R are reference-like in the sense that modifying a nested element through one variable does not affect other variables pointing to the same top-level list — R’s copy-on-modify semantics apply at the list level. However, if a list contains an environment, the environment is not copied; both the original and the copy share the same underlying environment object. This distinction matters when lists are used as function closures or when storing mutable state inside list elements.

See also