readr write_csv() in R: Export Data Frames to CSV
The readr write_csv() function writes a data frame to a comma-separated file. It is faster than base R's write.csv(), never adds a row-names column, and always writes clean UTF-8 text.
write_csv(df, "out.csv") # write a data frame to disk write_csv(df, "out.csv", na = "") # blanks instead of NA write_csv(df, "out.csv", append = TRUE) # add rows to an existing file write_csv2(df, "out.csv") # EU format: ; and decimal comma write_excel_csv(df, "out.csv") # add a BOM so Excel reads UTF-8 write_csv(df, "out.csv", quote = "all") # quote every value write_csv(df, stdout()) # print the CSV to the console
Need explanation? Read on for examples and pitfalls.
What write_csv() does
write_csv() turns a data frame into a CSV file. You give it a data frame and a path, and it writes one comma-separated line per row plus a header row of column names. It uses fast C++ code, encodes everything as UTF-8, and returns the input data frame invisibly so it slots into a pipe without breaking the chain.
Syntax and key arguments
The signature is short, and most calls only need the first two arguments. The rest control missing values, quoting, and whether you overwrite or extend an existing file.
The arguments you reach for most are na (declare how missing values look on disk), append (extend a file instead of replacing it), and quote (force or suppress quoting around values).
write_csv(df, "out.csv") is df.to_csv("out.csv", index=False). pandas writes the row index by default, so you must pass index=False; readr never writes row names, so there is nothing to switch off.write_csv() examples
Start with the simplest call. Build a small data frame, write it, and read it back to confirm the round trip works.
Write to stdout() to see the exact file contents. Passing a connection instead of a path prints the CSV to the console, which is the quickest way to inspect quoting and separators.
Use append = TRUE to add rows without rewriting the file. readr skips the header row when appending, so you do not get a duplicate header in the middle of the data.
Control how missing values are written with na. By default NA is written as the literal text NA. Set na = "" to leave empty cells, which many other tools expect.
write_csv() vs write.csv() and alternatives
write_csv() and base R's write.csv() produce different files from the same data. The biggest difference is row names: write.csv() adds an unnamed first column of row labels, while write_csv() never does. That single behavior causes most migration surprises.
| Feature | write_csv() (readr) | write.csv() (base R) | fwrite() (data.table) |
|---|---|---|---|
| Row names | Never written | Written by default | Never written |
| Speed | Fast (C++) | Slow | Fastest |
| Quoting | Only when needed | Quotes every text value | Only when needed |
| Encoding | Always UTF-8 | Locale-dependent | UTF-8 |
| Return value | The input, invisibly | NULL | NULL |
Use write_csv() for tidyverse workflows and pipe chains. Use data.table::fwrite() when the file has millions of rows and write speed dominates. Reach for write.csv() only when a downstream tool genuinely expects the row-names column.
Common pitfalls
Three mistakes cause most write_csv() bugs. Each one is silent, so the file looks fine until something downstream breaks.
Row names disappear. write_csv() drops row names without warning. Writing mtcars loses the car model labels because they live in the row names, not a column.
Appending blindly can corrupt a file. With append = TRUE, readr writes rows by column position and does not check that the names or order match the existing file. Always confirm the new data has the same columns in the same order.
Column types are not preserved. CSV is plain text, so factors, dates, and exact numeric types are lost on a round trip. Reading the file back re-guesses every column. When you need an exact round trip, use write_rds() instead.
Try it yourself
Try it: Write the built-in airquality data frame to a file called aq.csv, then read it back into ex_aq. Confirm it has 153 rows.
Click to reveal solution
Explanation: write_csv() saves the data frame to disk, and read_csv() reads it back into a tibble. The row count is unchanged because writing and reading a CSV never adds or drops rows.
Related readr functions
These functions pair naturally with write_csv() for import and export work:
- read_csv() reads a CSV file back into a tibble, the exact inverse of write_csv().
- write_tsv() writes a tab-separated file for tools that expect tabs.
- write_delim() writes any single-delimiter file, such as pipe-separated output.
- write_rds() saves an R object to disk with all column types preserved.
- write_lines() writes a character vector to a file, one string per line.
For the full argument reference, see the readr write_csv() documentation.
FAQ
What is the difference between write_csv() and write.csv()? The core difference is row names. Base R's write.csv() adds an unnamed first column holding row labels, while readr's write_csv() never writes row names. write_csv() is also faster, always encodes UTF-8, and quotes values only when needed instead of quoting every text field. For tidyverse pipelines, write_csv() is the cleaner default.
Does write_csv() add row names to the file? No. write_csv() writes only the columns of the data frame, never the row names. If your data has meaningful row names, such as the model labels in mtcars, promote them to a real column first with tibble::rownames_to_column() before writing, or they will be lost.
How do I append rows to a CSV file with write_csv()? Pass append = TRUE. readr then skips the header row and writes the new rows after the existing data. Make sure the new data has the same columns in the same order as the file, because append = TRUE writes by position and does not verify that the headers match.
How do I open a write_csv() file in Excel without encoding errors? Use write_excel_csv() instead. It writes the same data but prepends a UTF-8 byte-order mark, which tells Excel to read the file as UTF-8. Without it, Excel may display accented or non-Latin characters as garbled symbols.
Can write_csv() write a compressed file? Yes. If the file path ends in .gz, .bz2, .xz, or .zip, readr compresses the output automatically. For example, write_csv(df, "out.csv.gz") writes a gzip-compressed CSV, and read_csv() decompresses it transparently when you read it back.