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()) data frame (invisibly) · Updated March 20, 2026 · 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 localeswrite_excel_csv()— always quotes all fields, adds a UTF-8 byte order mark for Excel compatibilitywrite_excel_csv2()— semicolon delimiter with UTF-8 BOMwrite_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
| Argument | Type | Default | Description |
|---|---|---|---|
x | data frame | — | Data to write |
file | string | — | Output file path |
na | string | "NA" | String to use for missing values |
append | logical | FALSE | Append to existing file? |
col_names | logical | !append | Include column names in output? |
quote | string | "needed" | Quoting strategy: "needed", "all", or "none" |
escape | string | "double" | How to escape embedded quotes: "double", "backslash", or "none" |
eol | string | "\n" | End-of-line character |
num_threads | integer | readr_threads() | Number of parallel threads |
progress | logical | show_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
dplyr::count()— count observations by group before or after writingis.na()— detect missing values, useful for preprocessing before exporttidyr::pivot_longer()— reshape data before writing to CSV