Menu

Lists in R: Create, Access ([[ vs $ vs [) and Modify

Lists are R's anything-goes container: mixed types, nested structures, whole data frames. The key skill is knowing when [ returns a smaller list and when [[ returns the element itself.

This page includes runnable editors - edit, run, and see output instantly.

A List Holds Anything

A vector has one hard rule: every element must be the same type. A list has no such rule. Each element of a list can be a different type, a different length, even another list. It's R's general-purpose container:

length() reports 4 - four elements, even though one of them (scores) contains three numbers itself. A list counts its compartments, not what's inside them. str() (structure) is the fastest way to see what a list actually holds - one line per element with its name, type, and a preview. Make str() a reflex: whenever a function hands you something and you're not sure what it is, str() it.

Names are optional (list(1, "a", TRUE) is legal) but almost always worth having - a named list documents itself.

The Three Ways In: [, [[ and $

This is the part of lists that everyone gets wrong once, so let's get it exactly right. There are three access operators, and they do two different jobs:

  • [ returns a smaller list - a container of the same kind, holding whatever you selected.
  • [[ returns the element itself - the actual value inside the compartment.
  • $ is shorthand for [[ with a literal name: person$age is person[["age"]].

The difference is invisible when you print casually and very visible the moment you try to compute:

person["scores"] has class "list" - a list of length 1 with the scores vector inside it. person[["scores"]] has class "numeric" - the vector itself, ready for mean(). Call mean() on the single-bracket version and you get an error about the argument not being numeric; that error message is almost always a sign you used [ where you meant [[.

A metaphor that sticks: a list is a train. [ gives you a shorter train - still a train. [[ opens a carriage and hands you the cargo.

So when is [ the right tool? When you want several elements and want to keep them packaged:

You can't pull two elements "themselves" at once - two values need a container - so multi-element selection is always single-bracket, and always yields a list.

One more difference: $ uses partial matching (person$sc finds scores if it's unambiguous), which is convenient in the console and a liability in scripts. In code that has to keep working, prefer [[ with the full name - it also accepts a name stored in a variable, which $ can't do.

Adding, Changing, Removing

Lists grow and shrink by plain assignment - no special append method needed:

  • Assigning to a name that doesn't exist yet adds the element.
  • Assigning to an existing name replaces it.
  • Assigning to position length(x) + 1 appends an unnamed element.
  • Assigning NULL removes the element entirely - the list gets shorter. (This is the one place NULL acts as a deletion command; if you genuinely need to store "nothing" in a compartment, use x["k"] <- list(NULL).)

To glue two lists end to end, c() works on lists just as it does on vectors: c(list_a, list_b) returns one longer list.

Nested Lists

Because a list element can be another list, lists nest to any depth - which makes them R's natural shape for structured data like parsed JSON or grouped results:

Drilling in is just chaining accessors - each $ or [[ steps one level deeper. When a nested structure gets confusing, str(company) shows the whole tree at once, and str(company, max.level = 1) shows just the top layer.

Flattening with unlist()

unlist() collapses a list - however deeply nested - into a single vector:

Two things to notice. First, unlist() builds names for you (math1, math2, art) so you can still tell where each value came from. Second - and this is the trap - a vector holds only one type, so unlisting a mixed list coerces everything to the most flexible type present. The number 1 came back as the string "1". If you unlist and your numbers turn into text, the list wasn't as uniform as you thought.

Why Lists Matter

Lists can feel like a beginner topic you'll outgrow. It's the opposite - they're load-bearing across the whole language:

  • Functions that return several things return a list. R functions return one object; a list makes that one object carry a fitted value here, a coefficient table there. When you run a regression, the model object you get back is a (large, classed) list - str() on it proves it.
  • A data frame is a list of columns - equal-length vectors with some extra behavior on top. Everything you just learned ($, [[, assigning NULL to drop an element) applies verbatim to data frame columns.
  • The apply family - lapply() and friends - takes a list, applies a function to each element, and hands back a list. Lists in, lists out is the standard shape of repeated computation in R.

What You Take Away

  • Lists hold anything: mixed types, unequal lengths, other lists, whole data frames.
  • [ returns a smaller list; [[ returns the element itself; $ is [[ with a literal name. Reaching for math? You want [[ or $.
  • Add by assigning to a new name, remove by assigning NULL, combine with c().
  • unlist() flattens to a vector and coerces mixed types on the way - check the result's class.
  • Data frames are lists of columns, so this knowledge transfers directly.

Next up: matrices - the all-one-type, rows-and-columns counterpart to the list.

Frequently Asked Questions

What is the difference between [ and [[ in R?

Single brackets [ always return a list containing the selected elements - a smaller container of the same kind. Double brackets [[ reach inside and return the element itself. If x$scores holds a numeric vector, x["scores"] is a list of length 1 and x[["scores"]] is the numeric vector. Use [[ (or $) when you want to actually work with the value.

How do you add an element to a list in R?

Assign to a name that doesn't exist yet: x$email <- "rosa@example.com" or x[["email"]] <- .... The list grows automatically. To append without a name, assign to the next position: x[[length(x) + 1]] <- value. To remove an element, assign NULL to it: x$email <- NULL.

How do you convert a list to a vector in R?

unlist(x) flattens a list (including nested ones) into a single vector. Because a vector holds only one type, everything gets coerced to the most flexible type present - a list of numbers and strings becomes an all-character vector. Check the result's class() after unlisting.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED