Skip to the converter
Pandas to CSV

Guide

How to Append to a CSV File with Pandas

Appending is a two-key operation: the mode that keeps the old rows, and the header switch that keeps the column names from being written a second time.

df.to_csv("output.csv", mode="a", header=False, index=False)
Adds rows to the end of output.csv without touching what is already there.

With mode="a" pandas positions the write at the end of the file instead of clearing it first. header=False matters because the file almost certainly already has a header — leaving it on is how CSVs end up with the column names repeated halfway down. index=False keeps the row index out, matching what the first write should have done.

The converter below opens in append mode with these options selected: add rows and download the file to watch it grow, or copy the generated call.

Why the default is to overwrite, not append

to_csv() opens files in mode="w". Truncating is the default because the common case — regenerating a report, re-running an export — wants a clean file every time. There is no prompt, no backup, and no merge: the previous contents are gone the moment the write starts. If two runs of your script should build on each other, the append has to be asked for explicitly.

The reverse case — deliberately replacing the file — is simply the default at work:df.to_csv("output.csv", index=False) overwrites. Both behaviours live side by side in this one argument, which is why the mode is the first decision to make.

The first append: write or check for the header

The tricky moment is the transition from "file does not exist yet" to "file exists". A fixed header=False loses column names entirely when the file is new; a fixedheader=True duplicates the header row on every subsequent run.

To make the append idempotent, determine whether to write the header by checking file existence and file size dynamically before calling to_csv():

from pathlib import Path
import pandas as pd

output_path = Path("telemetry.csv")

# True if the file does not exist OR exists but is completely empty (0 bytes)
write_header = not output_path.exists() or output_path.stat().st_size == 0

df.to_csv(output_path, mode="a", header=write_header, index=False)
Idempotent append: writes column names on run #1, appends data-only rows on run #2 and beyond.

Checking st_size == 0 catches a common real-world edge case: files created empty by a prior step (like touch, an initial shell redirection, or an interrupted process). Without the size check, output_path.exists() would evaluate to True, producing a file that starts with data rows and has no column names at all.

Key limitations of the idempotent check

  • Concurrency and race conditions: If multiple processes, threads, or Celery workers append to the same file simultaneously, two workers can test not output_path.exists() at the same millisecond. Both will write headers, or worse, interleave partial rows. For concurrent writes, use OS-level file locking (such as portalocker or fcntl) or have each worker write a separate partition file.
  • No schema validation: The header check only decides whether column titles are printed. It does not ensure that the new DataFrame matches the column names, count, or ordering already on disk.
  • Encoding and delimiter consistency: Every append must match the original file's delimiter (sep), line terminators, and encoding. Appending UTF-8 data to an ISO-8859-1 file will corrupt the resulting document for future readers.

Appending safely: match the schema first

pandas writes rows, not relationships. It will happily append a frame with different columns, a different column order, or extra columns — producing a file that no standard reader can trust. Align the new data to the header already on disk before writing:

existing = pd.read_csv("output.csv", nrows=0).columns   # header only, no data read
df = df.reindex(columns=existing)                       # same columns, same order
df.to_csv("output.csv", mode="a", header=False, index=False)
nrows=0 reads just the header — cheap even for large files.

The same rule applies to the separator and encoding: the append must use whatever the first write used, or the file ends up with mixed delimiters.

Try append mode in the converter

The tool opens with Append rows selected — the generated code readsmode="a" and the header checkbox is off, which is the state you want after the first write. Switch to Overwrite to see the default (mode="w") return, and open More options to bring the header back for the file's very first write. The preview here is a single session, so download the CSV and append to it again to see the behaviour against a real file.

Add your data

Comma-separated (CSV)
Upload a file, paste your data, or use Pandas code.

You can also paste data copied from a spreadsheet.

Try an example:

Your data stays in your browser. Nothing is uploaded.

CSV options

Every control maps to a real to_csv() parameter, and the defaults are tuned for clean CSV output.
Basic options
Name for the downloaded file and written in the generated code.
Keep the first row as column names (header=True).
Include the DataFrame index as the first column.
Commas are the usual choice for CSV files.
UTF-8 is standard. No encoding= argument needed.
More options— formatting, columns, quoting, line endings
Formatting
Choose how decimal numbers should be written. Integer columns are left as integers.
Choose how missing values should appear in the CSV.
Applies strftime format to date columns.
Columns
All 3 columns

Selected columns are written in order as columns=[…] in generated code.

Advanced CSV settings
Controls which fields are wrapped in quotes.
Character used to quote a field. Default is a double quote.
Needed when quoting is disabled and a value contains the separator.
End-of-line character at the end of each row.
Header name for the row numbers column when enabled.
Append adds rows to an existing file instead of replacing it.
Character used for decimal points (use "," for European formats).

Your CSV is ready

CSV ready·2 rows · 3 columns·27 B·UTF-8
Append mode only affects the generated code. The file you download here always contains just this dataset.
Preview of the first 2 of 2 rows.
nameagecity
Dave35Pune
Eve27Jaipur

Showing all 2 rows.

Long values are clipped to keep rows readable — hover to see full cell. The downloaded output.csv contains every value in full.

Pandas code

The df.to_csv() call that reproduces these settings in Python.
df.to_csv("output.csv", header=False, index=False, mode="a")

Assumes your DataFrame is named df and that pandas is already imported.

Generated in this tab, nothing uploaded — download the file and append to it in your own script.

Common mistakes

  • Appending with the default mode. Forgetting mode="a" does not fail loudly — it overwrites the file with only the new rows.
  • Leaving header=True on the second write. The column names are stamped again where the appended rows begin, and every later reader trips over them.
  • Adding the index on append. index=True writes a fresh 0, 1, 2… sequence for the new rows, so the file gets a meaningless leading column that does not match the rows above it.
  • Assuming pandas aligns columns for you. It does not — a frame with reordered or extra columns corrupts the file's structure silently. Reindex to the existing header first.
  • Changing the separator between writes. The append is raw text; a different sep mid-file produces mixed delimiters.
  • Appending from two processes at once. There is no locking — concurrent writers can interleave or corrupt rows. Write to separate files and concatenate later.

Frequently asked questions

How do I append rows to an existing CSV file?

Open it in append mode and leave the header off: df.to_csv("output.csv", mode="a", header=False, index=False). mode="a" adds to the end of the file instead of replacing it, header=False stops a second copy of the column names from being written, and index=False keeps the extra index column out.

Why does the header row appear twice after appending?

Because header=True is the default and pandas cannot know the file already has one. Every write in append mode stamps the column names again at the point where the new rows begin. Pass header=False on every append after the first — or header=not Path("output.csv").exists() if the file may or may not be there yet.

What is the difference between mode="a" and mode="w"?

mode="a" appends to the existing file and leaves its contents in place. mode="w" is the default: it truncates the file first, so everything previously in it is gone and only the rows you write now remain. Appending is a choice you have to make — the default always overwrites.

Do the columns have to match the existing file?

pandas does not check. It writes whatever columns the DataFrame has, in its own order. If the new rows have different columns or a different order, the file becomes inconsistent for every reader afterwards — align the frame to the existing header with df.reindex(columns=...) before appending.

Can I append a DataFrame to a CSV with a different delimiter?

Yes — but pass the same separator on every call. mode="a" appends raw rows; if the first write used sep=";" and the append uses the default comma, the file ends up with mixed delimiters and no reader will parse it cleanly.

Is appending to a CSV safe for concurrent writers?

No. Two processes appending to the same file can interleave rows or corrupt the file — pandas has no locking. For concurrent writers, write each process to its own file and concatenate once at the end, or use a database instead of a shared CSV.

Related guides

Reviewed against the pandas 3.0.6 DataFrame.to_csv() reference — 6 October 2026.