Subsetting & Indexing
Reviewed & published by Brayan K
Subsetting is the act of pulling specific elements out of an R object using square brackets, with [ ] keeping a subset, [[ ]] extracting one element, and $ reaching a named element of a list.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end you'll confidently index by position, by negative position, by logical condition, and by name, and you'll know exactly when to use [ ] versus [[ ]] versus $ on vectors and lists.
What You'll Learn in This Lesson
- Positive, negative, logical, and name-based indexing
- The crucial difference between [ ], [[ ]], and $
- Why list subsetting differs from vector subsetting
- Filtering a vector with a logical condition
- Using which() to get positions instead of values
- Combining conditions with & and |
1️⃣ Indexing a Vector: Four Ways
Single brackets accept four kinds of index. A positive number selects by position, a vector of numbers selects several, a negative number drops positions, and a name (if your vector is named) selects by label. R indexes from 1.
# Single brackets [ ] subset a vector. Four ways to choose:
x <- c(a = 10, b = 20, c = 30, d = 40)
x[2] # by position -> b
# 20
x[c(1, 3)] # several -> a c
# 10 30
x[-1] # drop position 1 -> b c d
# 20 30 40
x["c"] # by name -> c
# 302️⃣ Logical Indexing & which()
The most powerful form: pass a logical vector and keep the elements where it is TRUE. The comparison itself (nums > 10) produces that logical vector. Use which() when you want the positions instead of the values, and combine tests with & (and) / | (or).
# Logical indexing keeps elements where the test is TRUE.
nums <- c(5, 12, 8, 20, 3)
nums > 10 # the test itself -> [1] FALSE TRUE FALSE TRUE FALSE
nums[nums > 10] # keep the TRUEs -> [1] 12 20
# which() turns a logical vector into the POSITIONS that are TRUE
which(nums > 10) # -> [1] 2 4
# Combine conditions with & (and) and | (or)
nums[nums > 4 & nums < 15] # -> [1] 5 12 83️⃣ Lists: [ ] vs [[ ]] vs $
Lists hold elements of different types, so the bracket you choose matters. [ ] keeps the list wrapper and returns a shorter list. [[ ]] reaches inside and returns the bare element. $ is a friendly shorthand for [[ ]] by name. Once you have the element with [[ ]] or $, you can index into it normally.
# Lists need different tools. [ ] keeps the list wrapper;
# [[ ]] and $ reach INSIDE to the element itself.
person <- list(name = "Ada", age = 36, langs = c("R", "C"))
person[1] # a LIST of length 1 (still wrapped)
person[[1]] # the VALUE inside -> [1] "Ada"
person$name # same thing, by name -> [1] "Ada"
person[["age"]] # by name with [[ ]] -> [1] 36
# $ and [[ ]] dig out the real object so you can use it:
person$langs[2] # second language -> [1] "C"Memorise the one-liner: [ ] keeps the box, [[ ]] and $ open the box.
Your turn. Fill in the ___ blanks, run it, and compare with the expected output.
# 🎯 YOUR TURN — replace each ___ then run it.
temps <- c(mon = 18, tue = 22, wed = 25, thu = 19, fri = 30)
# 1) Get Wednesday's temperature BY NAME
print(temps[___]) # 👉 replace ___ with "wed"
# 2) Keep only the days warmer than 20 (logical indexing)
warm <- temps[temps ___ 20] # 👉 replace ___ with >
print(warm)
# 3) Print the POSITIONS of those warm days with which()
print(which(temps > 20))
# ✅ Expected output:
# wed
# 25
# tue wed fri
# 22 25 30
# tue wed fri
# 2 3 5Mini-Challenge: Dig Into a List
This challenge forces you to switch between $, [[ ]], and [ ] on the same object — exactly the muscle memory you need for real data work.
# 🎯 MINI-CHALLENGE: dig into a nested list.
#
# 1. Build: order <- list(id = 1007,
# items = c("pen", "ink", "pad"),
# paid = TRUE)
# 2. Use $ to print the items vector.
# 3. Use [[ ]] to print just the SECOND item ("ink").
# 4. Use [ ] (single bracket) on items to keep the first and third.
# 5. Print which(nchar(order$items) == 3) to find 3-letter items.
#
# ✅ Example output:
# [1] "pen" "ink" "pad"
# [1] "ink"
# [1] "pen" "pad"
# [1] 1 2 3
# your code hereCommon Errors (and the fix)
- ❌ x[0] returns an empty result. ✅ R indexes from 1 — the first element is x[1].
- ❌ Doing math on person[1] fails (it's still a list). ✅ Use person[[1]] or person$name.
- ❌ Mixing signs: x[c(1, -2)] errors. ✅ Use all-positive or all-negative indices.
- ❌ Using && for element-wise filtering. ✅ Use single & / | inside brackets — && only compares the first element.
Pro Tips
- 💡 Assign into a subset: x[x < 0] <- 0 replaces every negative value with 0 in one line.
- 💡 %in% for membership: x[x %in% c(2, 4)] keeps values found in a set.
- 💡 head()/tail(): head(x, 3) grabs the first three without writing the indices.
📋 Quick Reference — Subsetting
| Task | R Syntax |
|---|---|
| By position | x[2] |
| Drop a position | x[-1] |
| By condition | x[x > 10] |
| Positions that match | which(x > 10) |
| List element (value) | lst[[1]] / lst$name |
| List subset (wrapped) | lst[1] |
🎉 Lesson Complete!
- ✅ Index a vector by position, negative position, condition, or name
- ✅ x[x > 10] filters; which() returns the positions
- ✅ [ ] keeps the box, [[ ]] and $ open it
- ✅ Combine conditions with single & and |
Practice quiz
In R, what does x[1] return for a vector x?
- The first element (R indexes from 1)
- An empty result
- The second element
- The whole vector
Answer: The first element (R indexes from 1). R is 1-indexed, so x[1] is the first element.
What does x[-1] do?
- Returns the last element
- Returns everything except the first element
- Errors
- Returns NA
Answer: Returns everything except the first element. A negative index drops that position, so x[-1] is all but the first.
What does x[0] return in R?
- The first element
- An error
- An empty vector of the same type
- NULL
Answer: An empty vector of the same type. Index 0 selects nothing, returning a length-0 vector of x's type.
Which operator extracts a single VALUE from a list, dropping the wrapper?
- [ ] single brackets
- Curly braces
- Parentheses
- [[ ]] double brackets
Answer: [[ ]] double brackets. [[ ]] reaches inside and returns the bare element; [ ] keeps the list wrapper.
What does lst$name do on a list?
- Returns a list of length 1
- Shorthand for lst[["name"]] by name
- Errors on lists
- Returns all elements
Answer: Shorthand for lst[["name"]] by name. $ is a friendly shorthand for [[ ]] by name on lists and data frames.
Given nums, what does nums[nums > 10] return?
- The positions where nums > 10
- The count of elements over 10
- The elements whose value exceeds 10
- TRUE or FALSE
Answer: The elements whose value exceeds 10. A logical vector inside [ ] keeps the elements where the test is TRUE.
What does which(nums > 10) return?
- The integer positions that are TRUE
- The matching values
- A single TRUE/FALSE
- The number of matches
Answer: The integer positions that are TRUE. which() converts a logical vector into the positions where it is TRUE.
Why does person[1] give a list but person[[1]] give the value?
- person[1] errors silently
- Single brackets keep the container; double brackets extract contents
- They are identical
- [[1]] keeps the wrapper
Answer: Single brackets keep the container; double brackets extract contents. [ ] preserves the list wrapper; [[ ]] extracts the element itself.
Which correctly combines two element-wise conditions inside [ ]?
- nums[nums > 4 && nums < 15]
- nums[nums > 4 AND nums < 15]
- nums[nums > 4 & nums < 15]
- nums[nums > 4 to 15]
Answer: nums[nums > 4 & nums < 15]. Single & is element-wise; && only compares the first element.
What happens with x[c(1, -2)] mixing positive and negative indices?
- Returns elements 1 and 2
- Drops element 2 only
- Returns the whole vector
- It is an error
Answer: It is an error. You cannot mix positive and negative indices in one bracket.
Continue this course
- Previous: Data Types & Classes
- Next: Missing Data (NA, na.rm) — Detect, drop and handle NA with is.na, na.rm and complete.cases
- Quick reference: R cheat sheet
Frequently asked questions
What is the difference between [ ], [[ ]], and $ in R?
Single brackets [ ] always return the same kind of object you started with — subsetting a vector gives a (shorter) vector, and subsetting a list gives a (shorter) list, keeping the wrapper. Double brackets [[ ]] reach inside and extract a single element as its own object, dropping the list wrapper. The dollar sign $ is a shorthand for [[ ]] by name that only works on lists and data frames: person$name is the same as person[["name"]]. Rule of thumb: [ ] to keep a subset, [[ ]] or $ to pull one thing out.
When should I use negative indexing?
A negative index drops elements rather than keeping them: x[-1] returns everything except the first element, and x[-c(1, 2)] drops the first two. It is concise when you want 'all but a few' positions. Two cautions: you cannot mix positive and negative indices in the same bracket, and negative indexing does not work with names — x[-"a"] is an error. For name-based removal, use x[!names(x) %in% "a"] instead.
What does which() do?
which() converts a logical vector (TRUE/FALSE) into the integer positions where the value is TRUE. So if nums > 10 gives FALSE TRUE FALSE TRUE, then which(nums > 10) returns 2 4 — the index positions. It is useful when you need the locations rather than the values, for example to update those positions or to find the index of a maximum with which.max(). You often do not need which() for filtering — nums[nums > 10] works directly on the logical vector.
Why does person[1] give a list but person[[1]] give the value?
This is the single-versus-double bracket rule applied to lists. person[1] uses single brackets, which preserve the container, so you get a list of length 1 that still wraps the value. person[[1]] uses double brackets, which extract the contents, so you get the bare value (a number, a character vector, etc.). If you try to do math or string operations and get a confusing 'list' error, you almost certainly used [ ] where you needed [[ ]] or $.