readr::write_csv()

write_csv(x, file, na = "NA", append = FALSE, col_names = !append, quote = "needed", escape = "double", eol = "\n", num_threads = readr_threads(), progress = show_progress())
Returns: data frame (invisibly) · Updated March 20, 2026 · Tidyverse
r readr csv data-export tidyverse

write_csv() writes a data frame to a comma-separated values file. It is part of the readr package, included in the core tidyverse. Compared to base R’s write.csv(), it is approximately twice as fast and never includes row names as a column.

library(readr)

df <- tibble(
  name = c("Alice", "Bob", "Carol"),
  age = c(30, 25, 40)
)
write_csv(df, "people.csv")

Four related functions handle specific formats:

  • write_csv2() — semicolon delimiter for European locales
  • write_excel_csv() — always quotes all fields, adds a UTF-8 byte order mark for Excel compatibility
  • write_excel_csv2() — semicolon delimiter with UTF-8 BOM
  • write_tsv() — tab-delimited output

Writing a CSV File

write_csv(df, "output.csv")

write_csv() returns the input data frame invisibly — it produces no printed output and is designed for its side effect. This matters in pipelines:

# WRONG: mtcars disappears from the pipe
mtcars |> write_csv("out.csv") |> nrow()
# NULL

# CORRECT: wrap in invisible()
mtcars |> invisible(write_csv("out.csv")) |> nrow()
# 32

Parameters

ArgumentTypeDefaultDescription
xdata frameData to write
filestringOutput file path
nastring"NA"String to use for missing values
appendlogicalFALSEAppend to existing file?
col_nameslogical!appendInclude column names in output?
quotestring"needed"Quoting strategy: "needed", "all", or "none"
escapestring"double"How to escape embedded quotes: "double", "backslash", or "none"
eolstring"\n"End-of-line character
num_threadsintegerreadr_threads()Number of parallel threads
progresslogicalshow_progress()Show progress bar in interactive sessions?

Common Usage Patterns

Basic write

write_csv(mtcars, "mtcars.csv")
# Contents:
# mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
# 21,6,160,110,3.9,2.62,16.46,0,1,4,4
# 21,6,160,110,3.9,2.62,16.46,0,1,4,4
# ...

Custom NA string

df <- tibble(x = 1:4, y = c("a", NA, "c", NA))
write_csv(df, "missing.csv", na = ".")
# x,y
# 1,a
# 2,.
# 3,c
# 4,.

Appending rows

# First batch
write_csv(df1, "data.csv")

# Append second batch — must have matching columns
write_csv(df2, "data.csv", append = TRUE)

When appending, col_names defaults to FALSE so the second chunk does not write a header row. If the data frame has different columns from the existing file, readr warns and fills missing columns with NA.

Controlling quotes

df <- tibble(text = c("normal", "has,comma", "has\"double"))

write_csv(df, "quoted.csv", quote = "needed")
# text
# normal
# "has,comma"
# "has""double"

write_csv(df, "all_quoted.csv", quote = "all")
# "text"
# "normal"
# "has,comma"
# "has""double"

Windows line endings

write_csv(df, "windows.csv", eol = "\r\n")

Automatic Compression

Append .gz, .bz2, or .xz to compress without external tools:

write_csv(mtcars, "mtcars.csv.gz")   # gzip
write_csv(mtcars, "mtcars.csv.bz2")  # bzip2
write_csv(mtcars, "mtcars.csv.xz")   # lzma

Compressed output uses a single thread automatically.

Output Details

write_csv() handles each type specifically:

  • Factors: coerced to character, written as strings
  • Doubles: formatted with the grisu3 algorithm (shortest exact decimal representation)
  • POSIXct: written as ISO8601 UTC. Local timezone datetimes are converted to UTC first.
  • UTF-8: all output is UTF-8 encoded
  • No row names: unlike write.csv(), row names are never written as a column

Gotchas

invisible() breaks pipelines. Since write_csv() returns its input invisibly, subsequent steps in a chain see NULL. Wrap it in invisible() to continue the pipeline.

NA strings are treated specially. Missing values (NA) are written as the na string and are never quoted. But a cell value that equals the na string is always quoted to distinguish it from a true missing value:

df <- tibble(x = c("NA", "real_value", NA))
write_csv(df, "na_test.csv", na = "NA")
# x
# "NA"        <- quoted: it's the literal string "NA"
# real_value
# NA          <- unquoted: it's a true NA

quote = "none" can corrupt output. If data contains commas, quotes, or newlines and quoting is suppressed, the file may not parse correctly:

df <- tibble(x = c("hello, world"))
write_csv(df, "bad.csv", quote = "none")
# Result: hello, world  <- not valid CSV

Appending requires matching columns. Mismatched columns produce a warning with NA fills — not an error, but the result may not be what you expect.

See Also