Cookbook
Pandas to_csv() Options, One Recipe at a Time
The export guide covers the call itself — where the file lands, what gets written, and how to verify it. This page is the other half: the arguments that change how values are formatted in the file, each as a runnable recipe with the text it produces.
The homepage's to_csv() options table gives every argument one line; here the same arguments get worked examples, the failure each one prevents, and the limit each one has. The recipes all write this frame:
import pandas as pd
df = pd.DataFrame({
"name": ["Alice, Jr.", "Bob"],
"score": [91.5, 87.2537],
"when": pd.to_datetime(["2026-01-02", "2026-03-04"]),
"id": ["007", "008"],
})One distinction runs through all of it: pandas writes CSV text, and the program that opens the file decides how to interpret that text. Each recipe below says which side of that line its behaviour lives on.
Quoting and escaping fields
The default is minimal quoting: pandas wraps a value only when the CSV rules require it — the separator or a quote character inside the value. "Alice, Jr." is quoted because of its comma; Bob is not. To quote every field whether it needs it or not:
import csv
df[["name", "score"]].to_csv("output.csv", index=False, quoting=csv.QUOTE_ALL)
# "name","score"
# "Alice, Jr.","91.5"
# "Bob","87.2537"Two details in that output do the real work. A quote character inside a value is doubled by default — that is what lets say "hi" survive being wrapped as"say ""hi""" — and the quote character itself is changeable withquotechar, so quotechar="' switches the file to single quotes.
At the other end, csv.QUOTE_NONE writes no quotes at all. That makesescapechar mandatory the moment a value contains the separator or a quote:
df[["name"]].to_csv("output.csv", index=False,
quoting=csv.QUOTE_NONE, escapechar="\\")
# name
# Alice\, Jr.
# Bobcsv.QUOTE_NONNUMERIC sits between the two: text fields are quoted, numbers are left bare — "Alice, Jr.",91.5 on the same row. It is also the closest thing to quoting specific columns, because quoting is one setting for the whole file: there is no per-column quoting argument. Control which fields get quoted by controlling which columns are strings.
Float precision, decimal places and scientific notation
float_format is one printf-style string that applies to every float column in the file. "%.2f" rounds to two decimal places:
df[["name", "score"]].to_csv("output.csv", index=False, float_format="%.2f")
# name,score
# "Alice, Jr.",91.50
# Bob,87.25Without it, each float is written at full precision. The same setting is also the way out of scientific notation: pandas writes small magnitudes with an exponent by default, and a fixed-point format removes it:
small = pd.DataFrame({"x": [0.0000012345, 12345678.9]})
small.to_csv("output.csv", index=False)
# x
# 1.2345e-06
# 12345678.9
small.to_csv("output.csv", index=False, float_format="%f")
# x
# 0.000001
# 12345678.900000The catch is scope: float_format is one setting for the whole file, and there is no per-column float format. The workaround is the same one quoting has — format the column before writing it, and the writer passes the text through untouched:
money = pd.DataFrame({"price": [19.99, 49.99], "rate": [0.123456, 0.987654]})
money["price"] = money["price"].map(lambda v: f"{v:.2f}")
money.to_csv("output.csv", index=False)
# price,rate
# 19.99,0.123456
# 49.99,0.987654One more formatting switch lives here: decimal="," swaps the decimal point character. Note what it produces — the values now contain the default separator, so pandas quotes every one of them:
df[["score"]].to_csv("output.csv", index=False, float_format="%.2f", decimal=",")
# score
# "91,50"
# "87,25"Compression: gzip and zip
Naming the file output.csv.gz is enough: pandas infers gzip from the extension, on write and on read. compression="gzip" says the same thing explicitly, for files whose names carry no hint:
df.to_csv("output.csv.gz", index=False) # gzip inferred from .gz
df.to_csv("snapshot", index=False, compression="gzip") # explicit
back = pd.read_csv("output.csv.gz") # also inferred on readcompression="zip" writes a zip archive containing one member, named after the target file. A compression dict renames that member and carries any further options:
df.to_csv("output.csv.zip", index=False, compression="zip")
# archive member: output.csv
df.to_csv("bundle.zip", index=False,
compression={"method": "zip", "archive_name": "data.csv"})
# archive member: data.csv
back = pd.read_csv("output.csv.zip") # zip inferred on readLine endings and blank lines
The argument is spelled lineterminator — no underscore. The near-missline_terminator raises a TypeError that names the correct spelling, which is the fastest route out of the typo:
df.to_csv("win.csv", index=False, lineterminator="\r\n") # CRLF on every row boundary
df.to_csv("unix.csv", index=False, lineterminator="\n") # LF on every row boundaryThe last row ends with a terminator like every other row, so a file finishing in a newline is complete — the "blank line" some editors draw at that position is the terminator itself, not an empty record. A genuine blank line between records takes a two-character terminator:
df[["name"]].to_csv("output.csv", index=False, lineterminator="\n\n")
# name
#
# "Alice, Jr."
#
# BobDate and datetime formatting
date_format takes a strftime format string and applies it to every datetime column during the write. By default, pandas exports datetimes in ISO 8601 format (%Y-%m-%d or %Y-%m-%d %H:%M:%S). Use date_format when downstream software expects a specific representation such as day-first dates or custom timestamp precision:
import pandas as pd
df = pd.DataFrame({
"event": ["login", "checkout"],
"timestamp": pd.to_datetime(["2026-10-16 09:30:15", "2026-10-16 14:15:22"]),
})
# Format timestamps during CSV export
df.to_csv("events.csv", index=False, date_format="%Y-%m-%d %H:%M:%S")The resulting events.csv contains the formatted text:
event,timestamp
login,2026-10-16 09:30:15
checkout,2026-10-16 14:15:22Formatting on export vs. changing the underlying data: date_formatcontrols only the serialization string written to disk. It does not modify the DataFrame in memory — df["timestamp"] remains a native datetime64[ns]Series, preserving date filtering, timezone math, and timestamp properties for subsequent code.
In contrast, if you convert the column using df["timestamp"] = df["timestamp"].dt.strftime("%Y-%m-%d %H:%M:%S"), you permanently alter the column's underlying data into a generic string object dtype. That conversion is only necessary when different datetime columns require different formats in the same file, because date_format is a single file-wide argument.
Scope rule: date_format applies strictly to datetime columns. If a column already holds strings like "2026-10-16", pandas treats it as plain text and ignoresdate_format entirely. Convert strings with pd.to_datetime() first if you need the export formatter to reshape them. On the read side, parse_datescompletes the round trip by parsing those strings back into datetime objects.
Keeping leading zeros
An identifier like 007 survives the write only if it is text when the write happens. A string column is written verbatim:
df[["id"]].to_csv("output.csv", index=False)
# id
# 007
# 008An integer column lost them long before to_csv() ran — the value is already7, and no writer argument can restore digits that are not in the data. The second half of the recipe is on the read side, where a naive read_csv()re-infers the integer type and drops the zeros again:
back = pd.read_csv("output.csv")
back["id"].dtype # int64 — the zeros are gone again
typed = pd.read_csv("output.csv", dtype={"id": str})
typed["id"].tolist() # ['007', '008']The read guide's dtype section covers when a column must be forced to text on the way in. The last mile is the spreadsheet, and it belongs to that program, not to pandas. Quoting the field — evenQUOTE_ALL, which writes "007" into the file — does not decide how Excel or Sheets imports it: the app's own import settings do, and the default double-click path turns "007" into the number 7 anyway. Keep the zeros in the file with the two steps above; import as Text when the file is opened in a spreadsheet.
Multi-character separators are not supported
sep must be a single character — any one, including a tab — and pandas checks that at call time:
df.to_csv("output.csv", index=False, sep="||")
# TypeError: "delimiter" must be a unicode character, not a string of length 2Two ways forward: write with a single character and post-process the file, or use a single character the consumer can split on. Multi-character separators exist on the read side —read_csv(engine="python") accepts them; theread guide's engine sectioncovers that half.
Frequently asked questions
Which to_csv() arguments does this page not cover?
The ones that choose what gets written rather than how it is formatted: sep, header, index, columns, encoding and mode are worked through in the export guide, and the homepage table lists every argument in one line each. This page covers the formatting arguments — quoting, float_format, compression, lineterminator and date_format — as full recipes.
How do I control the number format of exported values?
float_format is the one argument that formats numbers. float_format="%.2f" rounds every float column to two decimals in one call, and integer columns are left alone. It applies to the whole file, so for a different precision per column, convert that column to strings with the format you want before writing.
Why does my CSV file end with a blank line?
It does not have one. The file ends with the line terminator after the last row — every row, including the final one, ends in a newline. Editors that draw an empty final line are showing that terminator, and CSV readers ignore it.
How do I add a blank line between rows?
Set lineterminator="\n\n" — the row separator becomes two newlines, so a blank line lands between the header and each record. pd.read_csv() skips empty lines, so the file still reads back normally.
Can I use a multi-character separator like "||"?
No. sep must be a single character: sep="||" raises TypeError: "delimiter" must be a unicode character, not a string of length 2. Write with a single character and post-process the file if the two-character form is what the consumer expects.
Do I have to import csv to use quoting?
For the readable names, yes — quoting=csv.QUOTE_ALL needs import csv at the top of the script. The same styles are the plain integers 0 to 3 (0 minimal, 1 all, 2 non-numeric, 3 none), so quoting=1 is exactly csv.QUOTE_ALL if you would rather not import the module.
Reviewed against the pandas 3.0.6 DataFrame.to_csv() reference — 6 October 2026.