purrr imap() in R: Map Over Values and Their Names

The purrr imap() function iterates over a list or vector and its names or index at the same time, exposing each value as .x and each name as .y. Type-safe variants (imap_chr, imap_dbl, imap_dfr) return an atomic vector or data frame instead of a list.

⚡ Quick Answer
imap(x, ~ paste(.y, .x))             # list; .y = name or index
imap_chr(x, ~ paste0(.y, ": ", .x))  # character vector
imap_dbl(x, ~ .x * .y)               # numeric vector
imap_int(x, ~ .y)                    # integer index per element
imap_dfr(x, ~ data.frame(id = .y))   # row-bind to a data frame
iwalk(x, ~ cat(.y, .x, "\n"))        # side effects, no return
imap(unnamed, ~ .y)                  # .y is 1, 2, 3, ...

Need explanation? Read on for examples and pitfalls.

📊 Is imap() the right tool?
STARTneed value plus its name or indeximap(x, ~ paste(.y, .x))value only, no index neededmap(x, ~ .x * 2)two separate inputs in lockstepmap2(a, b, ~ .x + .y)index plus side effects onlyiwalk(x, ~ cat(.y))three or more inputspmap(list(a, b, c), sum)running total across elementsreduce(x, `+`)

What imap() does in one sentence

imap(.x, .f) calls .f once per element of .x, passing the element as .x and its name or integer position as .y. It is exactly map2(.x, names(.x) %||% seq_along(.x), .f), with the second input filled in for you.

When .x has names, .y is the name string. When .x is unnamed, .y is the integer index 1, 2, 3, .... That makes imap() the tool for any task where each result depends on both a value and where that value sits.

Syntax

imap(.x, .f, ...). .x is the list or vector; .f is the function or lambda; ... holds extra arguments passed to .f.

Inside a purrr lambda, .x is the current element and .y is its name or index. There is no separate names argument, because imap() derives .y from .x itself. A two-argument function works too, with the value first and the name second.

Run live
Run live, no install needed. Every R block on this page runs in your browser. Click Run, edit the code, re-run instantly. No setup.
Rimap exposes value and name
library(purrr) imap(c(a = 1, b = 2), ~ paste0(.y, " -> ", .x)) #> $a #> [1] "a -> 1" #> #> $b #> [1] "b -> 2"

  
Tip
Pick the typed variant whenever the output type is known. imap_chr() returns a character vector and errors if any call returns something else, catching bugs at the call site. Reserve bare imap() for results that are genuinely mixed types or must stay a list.

Five common patterns

1. Plain imap (list output)

imap() with no suffix returns a list the same length and names as .x. Use it when each call produces something that should not be flattened, such as a vector, a model, or a nested list.

RPrefix each score with its subject
scores <- list(math = 90, science = 85, art = 78) imap(scores, ~ paste(.y, "scored", .x)) #> $math #> [1] "math scored 90" #> #> $science #> [1] "science scored 85" #> #> $art #> [1] "art scored 78"

  

Each call sees the value as .x and the list name as .y, so the result keeps the original names.

2. Type-safe character output

imap_chr() collapses the result into a named character vector. It is the variant to reach for when every call returns a single string.

RType-safe character output
imap_chr(scores, ~ paste0(.y, ": ", .x)) #> math science art #> "math: 90" "science: 85" "art: 78"

  

The names of scores carry through to the names of the returned vector.

3. imap over an unnamed vector

On an unnamed input, .y is the integer position, not a name. This turns imap() into a clean way to number elements without writing a seq_along() loop.

RNumber elements of an unnamed vector
fruits <- c("apple", "banana", "cherry") imap_chr(fruits, ~ paste0(.y, ". ", .x)) #> [1] "1. apple" "2. banana" "3. cherry"

  

Here .y takes the values 1, 2, and 3 because fruits has no names.

4. Combine results into a data frame

imap_dfr() calls a function that returns a data frame, then row-binds every result. It builds one tidy table from a list and its names.

RBuild a data frame, one row per element
imap_dfr(scores, ~ data.frame(subject = .y, score = .x)) #> subject score #> 1 math 90 #> 2 science 85 #> 3 art 78

  

The list name becomes a column value, which is the usual reason to switch from a named list to a data frame.

5. iwalk for side effects

iwalk() is the side-effect version of imap(). It runs an index-aware function for printing, plotting, or writing files, and returns the input invisibly.

RPrint each item for its side effect
iwalk(scores, ~ cat(.y, "=", .x, "\n")) #> math = 90 #> science = 85 #> art = 78

  

Use iwalk() instead of imap() when you do not need the return value, only the action it performs.

Key Insight
imap(x, f) is just map2(x, names(x) %||% seq_along(x), f). purrr fills the second input for you: names when x has them, integer positions when it does not. That single rule explains every imap() result. If .y shows up as 1, 2, 3 when you expected text, your input has no names.

imap() vs map2() vs map()

Three purrr functions cover index-aware iteration, differing in where the index comes from. imap() derives it automatically, while map2() needs it spelled out.

Approach Second input When to use
map(x, .f) none the result needs only the value
imap(x, .f) names(x) or seq_along(x), automatic the result needs the value and its label
map2(x, names(x), .f) explicit, you supply it the labels come from somewhere other than x
Map(f, x, names(x)) explicit, base R no tidyverse dependency wanted

Reach for imap() when the index you want is the one already attached to .x. Reach for map2() when the second input is a separate object, such as a vector of file paths or a lookup table.

Note
Coming from Python? imap(x, f) on an unnamed input behaves like enumerate(x), and on a named list like iterating dict.items(). In both cases you get the value and its position or key together in one pass.

Common pitfalls

Pitfall 1: assuming .y is always a name. On an unnamed vector, .y is the integer index, not a string. Code like imap_chr(unnamed, ~ toupper(.y)) fails because toupper() cannot uppercase a number. Check whether the input is named before relying on .y.

Pitfall 2: confusing .y with a second value. In map2(), .y is the matching element of a second input. In imap(), .y is the name or index of the same element. Carrying a map2() mental model into imap() leads to treating a label as data.

Pitfall 3: partially named lists. imap() only falls back to integer positions when names(.x) is entirely missing. If a list has names on some elements and not others, the unnamed ones give .y as "", an empty string, not an index. Name every element or none.

Try it yourself

Try it: Use imap_chr to turn the named vector c(a = 5, b = 10, c = 15) into strings like "a = 5". Save the result to ex_tagged.

RYour turn: tag values with imap
# Try it: imap_chr over a named numeric vector ex_tagged <- # your code here ex_tagged #> Expected: 3 strings pairing each name with its value

  
Click to reveal solution
RSolution
ex_tagged <- imap_chr(c(a = 5, b = 10, c = 15), ~ paste0(.y, " = ", .x)) ex_tagged #> a b c #> "a = 5" "b = 10" "c = 15"

  

Explanation: imap_chr() walks the vector, exposing each value as .x and each name as .y. The lambda pastes them into one string per element, and the _chr suffix returns a character vector instead of a list.

After imap, these functions round out index-aware and multi-input iteration:

  • imap_chr(), imap_dbl(), imap_int(), imap_lgl(): type-safe variants returning atomic vectors
  • imap_dfr(), imap_dfc(): combine results by row or column into a data frame
  • iwalk(): run an index-aware function for side effects, returning the input invisibly
  • map2(): iterate over two inputs when the second is a separate object, not names
  • map(): single-input iteration when no index is needed
  • pmap(): iterate over three or more inputs supplied as a list

The official argument reference lives in the purrr imap documentation.

FAQ

What is the difference between map and imap in purrr?

map() iterates over a single input and exposes each element as .x, with no access to position. imap() iterates over the same input but also exposes each element's name or integer index as .y. Use map() when the result depends only on the value, and imap() when it also depends on the label, such as numbering items or prefixing each value with its key.

What is .y in imap?

.y is the index of the current element. When the input has names, .y is the name string. When the input is unnamed, .y is the integer position: 1 for the first element, 2 for the second, and so on. This is the difference from map2(), where .y is an element of a separate second input rather than a label.

Does imap work on unnamed lists and vectors?

Yes. On an unnamed input, imap() uses seq_along() to supply integer positions as .y. So imap_chr(c("a", "b"), ~ paste0(.y, .x)) returns "1a" and "2b". This makes imap() a clean replacement for manual seq_along() loops when you need both the value and its index.

What is the difference between imap and map2?

imap(x, f) is shorthand for map2(x, names(x) %||% seq_along(x), f). Both expose .x and .y, but imap() derives .y from the input itself, while map2() takes it as an explicit second argument. Use imap() when the second input is the names or index of the first, and map2() when it is a separate object.

How do I use imap to write multiple files?

Use iwalk(), the side-effect version of imap(). With a named list of data frames, iwalk(dfs, ~ write.csv(.x, paste0(.y, ".csv"))) writes one file per element, naming each file after the list name. iwalk() returns the input invisibly, so it chains cleanly inside a pipeline.