rguides

purrr::walk()

walk(.x, .f, ...)

The walk() and walk2() functions from the purrr package apply a function to each element of a list or vector for its side effects—they return the input .x invisibly instead of collecting results. This is useful when you want to perform an action (printing, saving, plotting) on each element without constructing a new list. walk() iterates over a single vector, while walk2() iterates over two vectors in parallel, matching elements by position. Together they provide a clean, pipe‑friendly way to execute loops that produce side effects, keeping your code readable and functional.

Syntax

walk(.x, .f, ...)

walk2(.x, .y, .f, ...)

Parameters

ParameterTypeDefaultDescription
.xA vector or list(required)First vector to iterate over.
.yA vector or list(required)Second vector to iterate over (for walk2 only).
.fA function, formula, or vector(required)Function to apply to each element (or pair). If a formula, e.g. ~ .x + 1, it is converted to a function with .x (and .y for walk2) as arguments.
...Additional argumentsNoneExtra arguments passed on to .f.

Examples

Basic walk() for printing

library(purrr)

# Create a list of data frames
df_list <- list(
  mtcars[1:3, ],
  iris[1:3, ],
  airquality[1:3, ]
)

# Print each data frame (side effect)
walk(df_list, print)
#                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
# Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
# Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
# Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          5.1         3.5          1.4         0.2  setosa
# 2          4.9         3.0          1.4         0.2  setosa
# 3          4.7         3.2          1.3         0.2  setosa
# 
#   Ozone Solar.R Wind Temp Month Day
# 1    41     190  7.4   67     5   1
# 2    36     118  8.0   72     5   2
# 3    12     149 12.6   74     5   3

# The walk() call returns the original df_list invisibly,
# allowing further piping.
result <- walk(df_list, print)
class(result)  # "list"
length(result) # 3

The next example demonstrates walk() with anonymous function for side effects, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.

walk() with anonymous function for side effects

# Create a directory of CSV files
temp_dir <- tempdir()
walk(1:3, ~ write.csv(mtcars[1:3, ], file.path(temp_dir, paste0("mtcars_", .x, ".csv"))))

# List the created files
list.files(temp_dir, pattern = "mtcars_.*\\.csv")
# [1] "mtcars_1.csv" "mtcars_2.csv" "mtcars_3.csv"

The next example demonstrates walk2() for parallel iteration, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.

walk2() for parallel iteration

# Two vectors: file names and data frames
file_names <- c("cars.csv", "iris.csv", "air.csv")
data_list <- list(mtcars, iris, airquality)

# Save each data frame with its corresponding name
walk2(data_list, file_names, ~ write.csv(.x, .y))

# Check that files exist
file.exists(file_names)
# [1] TRUE TRUE TRUE

The next example demonstrates walk2() with formula syntax, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.

walk2() with formula syntax

# Create a series of simple scatter plots
x_vals <- list(1:10, 11:20, 21:30)
y_vals <- list(rnorm(10), rnorm(10), rnorm(10))

walk2(x_vals, y_vals, ~ plot(.x, .y, main = paste("Plot for", .y[1])))
# Three plot windows appear (side effect).

The next example demonstrates iterating with side effects in a pipeline, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.

Common patterns

Iterating with side effects in a pipeline

Because walk() returns its input invisibly, you can insert side‑effect steps into a pipeline without breaking the flow:

library(dplyr)
library(purrr)

mtcars %>%
  split(.$cyl) %>%
  walk(~ print(summary(.$mpg))) %>%
  map(~ lm(mpg ~ wt, data = .)) %>%
  walk2(c("cyl4", "cyl6", "cyl8"), ~ {
    png(paste0(.y, ".png"))
    plot(.x)
    dev.off()
  })

The next example demonstrates validating data without storing results, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.

Validating data without storing results

Use walk() to run validation checks, throwing an error if any check fails:

validate_data <- function(df) {
  walk(names(df), ~ if (any(is.na(df[[tidyverse::]]))) stop("Column ", .x, " contains NA"))
  walk(names(df), ~ if (!is.numeric(df[[tidyverse::]])) warning("Column ", .x, " is not numeric"))
  invisible(df)
}

validate_data(mtcars)  # passes silently
# validate_data(iris)  # would warn about Species column

The next example demonstrates batch file operations, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.

Batch file operations

Combine walk() with fs::dir_create() and walk2() with readr::write_csv() to create and populate directories:

library(fs)
library(readr)

# Create a directory for each species
species <- unique(iris$Species)
walk(species, ~ dir_create(.x))

# Write a CSV for each species into its directory
iris_split <- split(iris, iris$Species)
walk2(iris_split, species, ~ write_csv(.x, path(.y, "data.csv")))

purrr::walk() in practice

walk() applies a function to each element of a list or vector for its side effects and returns the input invisibly. Use it when the operation modifies external state, writing files, sending requests, printing output, and you do not need the return values. walk(files, write_csv) writes each data frame to a file without accumulating a list of return values.

walk2() is the two-input variant, equivalent to map2() but for side effects: walk2(dfs, filenames, write_csv) writes each data frame to the corresponding file. pwalk() is the parallel multi-input version.

walk() returns its input invisibly, which means it can be inserted into a pipe for side effects without breaking the pipeline: data |> walk(print) |> bind_rows() prints each element before combining them. The data flows through unchanged.

A common pattern is walk(plots, ggsave) or walk2(plots, filenames, ggsave) for batch-saving ggplot objects. Another is walk(connections, dbDisconnect) for closing a list of database connections after use. Anything that requires looping over a list for a side-effect operation is a candidate for walk().

Unlike map(), using walk() signals intent: this loop runs for side effects, not to collect results. This makes code more readable, a reader seeing walk() knows immediately that the return values are not used.

See also