# Base R only. readr is used once, as readr::read_csv().Lists
R’s most flexible container — and the key to reading the results of any analysis
By the end of this lesson you will be able to:
- create a list containing objects of different types and sizes, and name its elements;
- choose correctly between
[,[[and$to extract elements; - add, modify and remove elements, including in nested lists;
- inspect any list with
str()andnames(); - extract specific results (p-value, confidence interval) from the list returned by a statistical function such as
t.test().
1. Why lists?
Vectors and matrices hold values of one type, arranged in a regular shape. Real results are messier. Think of what a statistical test produces: a test statistic (one number), degrees of freedom (one number), a p-value (one number), a confidence interval (two numbers), the group means (two named numbers), the name of the method (text)…
R needs a container that can hold anything, of any size, together. That container is the list. Lists are everywhere in R, often hidden: every model, every test result and every data frame is, underneath, a list. Learning to navigate lists is what lets you go from looking at a printed output to using the numbers in it.
2. Creating a list
Use list(), ideally giving each element a name:
vect <- 1:5
mat <- matrix(1:10, nrow = 2, ncol = 5)
df <- as.data.frame(EuStockMarkets)[1:10, ] # stock indices, first 10 daysmy_list <- list(
vect = vect,
mat = mat,
df = df
)
names(my_list)#> [1] "vect" "mat" "df"
length(my_list)#> [1] 3
The list has three elements of completely different kinds: an integer vector of length 5, a 2 x 5 matrix and a data frame with 10 rows and 4 columns. length() counts the elements of the list (3), not what is inside them.
Looking inside: str()
Printing a list can produce pages of output. str() (structure) gives a compact overview, and is the single most useful function for lists:
str(my_list)#> List of 3
#> $ vect: int [1:5] 1 2 3 4 5
#> $ mat : int [1:2, 1:5] 1 2 3 4 5 6 7 8 9 10
#> $ df :'data.frame': 10 obs. of 4 variables:
#> ..$ DAX : num [1:10] 1629 1614 1607 1621 1618 ...
#> ..$ SMI : num [1:10] 1678 1688 1679 1684 1687 ...
#> ..$ CAC : num [1:10] 1773 1750 1718 1708 1723 ...
#> ..$ FTSE: num [1:10] 2444 2460 2448 2470 2485 ...
List of 3: the object is a list with three elements.- Each
$ name :line is one element, followed by its type and size:int [1:5](integer vector of length 5),int [1:2, 1:5](2 x 5 integer matrix),'data.frame': 10 obs. of 4 variables. - Lines starting with
..$are one level deeper: here, the columns of the data frame.
3. [, [[ and $: getting things out
This is the part of lists that confuses everyone at first. A good mental picture is a train: the list is the train, and each element is a carriage with some cargo inside.
| Code | Train analogy | Returns |
|---|---|---|
my_list[1] |
detach carriage 1 — you get a (shorter) train | a list of length 1 |
my_list[[2]] |
open carriage 2 and take out its cargo | the element itself (here, the matrix) |
my_list$df |
same as [[, using the carriage’s name |
the element itself (here, the data frame) |
my_list[c(1, 3)] |
detach carriages 1 and 3 — still a train | a list of length 2 |
my_list[1]#> $vect
#> [1] 1 2 3 4 5
class(my_list[1])#> [1] "list"
my_list[1] is still a list — notice the $vect label in the output: you are looking at a train with one carriage.
my_list[[2]]#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1 3 5 7 9
#> [2,] 2 4 6 8 10
class(my_list[[2]])#> [1] "matrix" "array"
my_list[[2]] is the matrix itself, so you can now use it as a matrix, for example to extract one of its elements:
my_list[[2]][2, 3] # row 2, column 3 of the matrix#> [1] 6
my_list[["vect"]][3] # 3rd element of the vector, by name#> [1] 3
head(my_list$df, 3) # $ is shorthand for [["df"]]#> DAX SMI CAC FTSE
#> 1 1629 1678 1773 2444
#> 2 1614 1688 1750 2460
#> 3 1607 1679 1718 2448
The difference matters as soon as you compute something:
my_list[[1]] + 100 # works: a vector plus a number#> [1] 101 102 103 104 105
my_list[1] + 100 # fails: you cannot add a number to a train#> Error in `my_list[1] + 100`:
#> ! argomento non numerico passato ad un operatore binario
$ versus [[
$ only works with a literal name (my_list$df), while [[ also accepts a variable: which_one <- "df"; my_list[[which_one]]. This makes [[ the right choice inside functions and loops. Also, $ on lists does partial matching: my_list$ve silently returns the vect element. [[ matches exact names only, which is safer. Finally, asking for a name that does not exist returns NULL with both $ and [["name"]], whereas a position that does not exist (my_list[[10]]) is an error.
4. Modifying, adding and removing elements
Assignment with $ or [[ changes an element, or adds it if the name is new. Assigning NULL removes it.
my_list$vect <- 6:10 # replace an element
my_list$note <- "first 10 days" # add a new element
names(my_list)#> [1] "vect" "mat" "df" "note"
my_list$note <- NULL # remove it
names(my_list)#> [1] "vect" "mat" "df"
You can also combine lists with c(), and turn a list of simple values into a vector with unlist():
c(list(a = 1), list(b = "x")) |> str()#> List of 2
#> $ a: num 1
#> $ b: chr "x"
unlist(list(a = 1, b = 2:3))#> a b1 b2
#> 1 2 3
unlist() follows the coercion rules of vectors: unlist(list(1, "a")) gives the character vector "1" "a".
5. Nested lists
A list element can itself be a list. This is how R stores complex, hierarchical information:
adelie <- list(
species = "Adelie",
stats = list(
n = 152,
islands = c("Biscoe", "Dream", "Torgersen")
)
)
str(adelie)#> List of 2
#> $ species: chr "Adelie"
#> $ stats :List of 2
#> ..$ n : num 152
#> ..$ islands: chr [1:3] "Biscoe" "Dream" "Torgersen"
To reach something deep inside, chain the extractors from left to right — one step per level:
adelie$stats$n#> [1] 152
adelie[["stats"]][["islands"]][2]#> [1] "Dream"
adelie$stats$islands[2] <- "Dream Island" # modify deep inside
adelie$stats$islands#> [1] "Biscoe" "Dream Island" "Torgersen"
6. Lists you already use without knowing it
A data frame is a list of columns
is.list(penguins)#> [1] TRUE
length(penguins) # number of columns#> [1] 8
penguins[["species"]][1:3] # [[ extracts a column, like $#> [1] Adelie Adelie Adelie
#> Levels: Adelie Chinstrap Gentoo
A data frame is a list whose elements (the columns) all have the same length. That is why penguins$body_mass and penguins[["body_mass"]] work, and why length() of a data frame is its number of columns. The same holds for data read from a file — for example the course dataset on US prisons:
prison <- readr::read_csv(data_path("prison.csv"), show_col_types = FALSE)
is.list(prison)#> [1] TRUE
length(prison) # 46 columns#> [1] 46
str(prison[, 2:6]) # all rows, columns 2 to 6#> tibble [714 × 5] (S3: tbl_df/tbl/data.frame)
#> $ state : num [1:714] 1 1 1 1 1 1 1 1 1 1 ...
#> $ year : num [1:714] 80 81 82 83 84 85 86 87 88 89 ...
#> $ govelec: num [1:714] 0 0 1 0 0 0 1 0 0 0 ...
#> $ black : num [1:714] 0.256 0.256 0.255 0.255 0.255 ...
#> $ metro : num [1:714] 0.632 0.636 0.64 0.645 0.649 ...
We will study data frames in depth in Data frames, and importing files in Import.
A test result is a list
This is where lists become really useful. Run a Welch t-test comparing the body mass of Adelie and Gentoo penguins (see t-tests):
two_sp <- penguins[penguins$species %in% c("Adelie", "Gentoo"), ]
two_sp$species <- droplevels(two_sp$species)
res <- t.test(body_mass ~ species, data = two_sp)
res#>
#> Welch Two Sample t-test
#>
#> data: body_mass by species
#> t = -23, df = 250, p-value <2e-16
#> alternative hypothesis: true difference in means between group Adelie and group Gentoo is not equal to 0
#> 95 percent confidence interval:
#> -1491 -1260
#> sample estimates:
#> mean in group Adelie mean in group Gentoo
#> 3701 5076
The printed output is designed for humans. But res is a list, and you can take any piece of it:
is.list(res)#> [1] TRUE
names(res)#> [1] "statistic" "parameter" "p.value" "conf.int" "estimate"
#> [6] "null.value" "stderr" "alternative" "method" "data.name"
res$p.value#> [1] 7.71e-65
res$conf.int#> [1] -1491 -1260
#> attr(,"conf.level")
#> [1] 0.95
res$estimate#> mean in group Adelie mean in group Gentoo
#> 3701 5076
names(res) lists the components of the result. The most useful are statistic (the t value), parameter (degrees of freedom), p.value, conf.int (the confidence interval, with an attribute recording its level), and estimate (the two group means). Because these are ordinary R objects, you can compute with them — for example, the estimated difference is diff(res$estimate), about 1375 g — or insert them into a report, as this course does with inline code. Model objects from lm() and glm() work the same way.
7. A first taste of lapply()
Often you want to do the same thing to every element of a list. lapply() (“list apply”) applies a function to each element and returns a list of results:
measurements <- list(
bill = penguins$bill_len,
flipper = penguins$flipper_len
)
lapply(measurements, mean, na.rm = TRUE)#> $bill
#> [1] 43.92
#>
#> $flipper
#> [1] 200.9
Extra arguments after the function name (na.rm = TRUE) are passed to the function. Its sibling sapply() simplifies the result to a vector when possible: sapply(measurements, mean, na.rm = TRUE). These functions, and their modern purrr equivalents, are covered in Iteration.
[when you meant[[.my_list[1]is a one-element list, not the element. If a function complains about receiving a list, check your brackets.$with a variable.name <- "df"; my_list$namelooks for an element literally calledname. Usemy_list[[name]].- Partial matching with
$.my_list$vesilently returnsmy_list$vect. Always write full names. - Printing a huge list. Use
str()(orstr(x, max.level = 1)) instead. - Copying numbers from printed output. Extract them from the result (
res$p.value) instead of retyping them: no typos, and they update when the data change.
8. Check your understanding
9. Exercises
Interactive (run in your browser)
★ Exercise 1 — build and inspect. Create a list info with three elements: course (the text "R for beginners"), year (the number 2026) and topics (the vector c("vectors", "factors", "lists")). Then inspect it with str() and print the second topic.
Text goes in quotes; the topics are a vector created with c().
info <- list(
course = "R for beginners",
year = 2026,
topics = c("vectors", "factors", "lists")
)
str(info)
info$topics[2] # "factors"★★ Exercise 2 — predict the output: [ vs [[. For each line, predict whether the result is a list or something else, then run the code to check.
Single brackets always return a list. x$c is the inner list; x$c$d is what is inside it.
class(x["a"]) # "list"
class(x[["a"]]) # "integer"
class(x$c) # "list" (the element itself is a list)
class(x$c$d) # "logical"
length(x[c("a", "b")]) # 2
sum(x[["a"]]) # 6sum(x["a"]) would fail: you cannot sum a list.
★★ Exercise 3 — extract results from a test. Using the sleep data, run a paired t-test comparing the extra sleep under drug 2 and drug 1, store the result in res and extract (a) the p-value, (b) the confidence interval, (c) the mean difference.
Look at names(res): you need p.value, conf.int and estimate.
res$p.value # about 0.003
res$conf.int # about 0.70 to 2.46 hours
res$estimate # mean difference about 1.58 hoursOn average, patients slept about 1.6 hours more with drug 2. Having these numbers as objects lets you round them, combine them or put them in a table.
★★ Exercise 4 — debug. The code below should add a new element sd to the list summary_list and then compute the coefficient of variation (sd / mean). It fails. Find and fix the two problems.
summary_list$stat looks for an element literally called stat. And summary_list[1] is a list, not a number.
summary_list[[stat]] / summary_list[["mean"]] # about 0.19Use [[stat]] to extract by the name stored in a variable, and [[ (or $mean) to get the number rather than a one-element list. Body mass varies by about 19% of its mean.
Practice on your own computer
★★★ Exercise 5 — a results table from a list. For each penguin species, run a one-sample t-test of flipper length against 200 mm. Use split() to make a list of flipper lengths by species, lapply() to run the three tests, and then sapply() to extract the three p-values and the three sample means.
fl <- split(penguins$flipper_len, penguins$species) # a list of 3 vectors
str(fl)
tests <- lapply(fl, t.test, mu = 200) # a list of 3 test results
sapply(tests, \(r) r$p.value)
sapply(tests, \(r) r$estimate)split() returns a named list; lapply() keeps the names, so the results are labelled by species. \(r) r$p.value is a short anonymous function: “given a result r, return its p-value” (see Functions). Adelie and Chinstrap have mean flipper lengths below 200 mm, Gentoo well above. (t.test() drops missing values automatically.)
Summary
| Function / syntax | What it does |
|---|---|
list(a = ..., b = ...) |
Create a (named) list |
names(), length(), str() |
Element names, number of elements, structure |
x[i] |
Sub-list (still a list) — “detach carriages” |
x[[i]], x[["name"]] |
The element itself — “take out the cargo” |
x$name |
Shorthand for x[["name"]] |
x$new <- value, x$name <- NULL |
Add / remove an element |
unlist() |
Flatten a list into a vector |
res$p.value, res$conf.int |
Extract results from a test |
lapply(), sapply() |
Apply a function to every element |
- Lists hold objects of any type and size.
[returns a list;[[and$return the element.- Data frames, test results and models are all lists:
str()andnames()reveal their contents. - Extract numbers from results instead of copying them by hand.