Write a List to CSV in Python (Row, Column or Table)

Writing a Python list to CSV takes three lines with the built-in csv module:

import csv

with open("out.csv", "w", newline="") as f:
    csv.writer(f).writerow(["apple", "pear", "plum"])

That newline="" is not decoration. Leave it out and every row in your file gets a blank line after it, which is the most common complaint about CSV in Python.

What shape the data lands in depends on which method you call, so this guide walks through a flat list, a column, a list of lists and a list of dictionaries. Runs are on Python 3.12.5.

Diagram comparing csv writerow and writerows output shapes for a flat Python list and a list of lists
The same module, four different shapes in the file.

Write a flat list to one row

writerow() takes a single row and spreads the list across it:

import csv

fruits = ["apple", "pear", "plum"]

with open("fruits.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(fruits)

print("file contents:")
print(open("fruits.csv").read())
print("one call to writerow puts the whole list across a single row.")

Output:

file contents:
apple,pear,plum

one call to writerow puts the whole list across a single row.
Command Prompt showing a flat Python list written to a single row of a CSV file
One writerow() call puts the whole list on one line.

The with block closes the file for you, which matters because CSV content is buffered and may not reach disk until the file closes.

Write a list to a CSV column

For one item per line, wrap each item in its own list:

import csv

fruits = ["apple", "pear", "plum"]

with open("column.csv", "w", newline="") as f:
    writer = csv.writer(f)
    for fruit in fruits:
        writer.writerow([fruit])

print("file contents:")
print(open("column.csv").read())
print("each item is wrapped in its own list, so each becomes its own row.")
print()
print("the square brackets are the whole trick: writerow expects a ROW,")
print("and a row is a sequence of cells.")

Output:

file contents:
apple
pear
plum

each item is wrapped in its own list, so each becomes its own row.

the square brackets are the whole trick: writerow expects a ROW,
and a row is a sequence of cells.

A row is a sequence of cells, so ["apple"] is a row with one cell in it. Passing the bare string instead is exactly the mistake covered two sections down.

Write a list of lists to CSV

This is the normal case for tabular data, and writerows() handles it in one call:

import csv

table = [
    ["name", "quantity", "price"],
    ["apple", 3, 0.55],
    ["pear", 5, 0.80],
    ["plum", 12, 0.25],
]

with open("table.csv", "w", newline="") as f:
    csv.writer(f).writerows(table)

print("file contents:")
print(open("table.csv").read())
print("writerows takes a list of lists and writes one row per inner list.")

Output:

file contents:
name,quantity,price
apple,3,0.55
pear,5,0.8
plum,12,0.25

writerows takes a list of lists and writes one row per inner list.

Each inner list becomes a row and each item inside it becomes a cell. Numbers are converted to text automatically, so mixing strings and floats needs no preparation.

Putting the header in as the first inner list is the simplest approach. Splitting a list into chunks first is useful when one file per batch is wanted.

writerow vs writerows

Mixing these two up produces a file that looks bizarre rather than an error:

import csv

fruits = ["apple", "pear", "plum"]      # a FLAT list

with open("oops.csv", "w", newline="") as f:
    csv.writer(f).writerows(fruits)     # writerowS, by mistake

print("what you get:")
print(open("oops.csv").read())
print("every string was treated as a row, and a string is a sequence")
print("of characters, so each letter landed in its own cell.")
print()
print("writerow  takes ONE row:        writer.writerow(fruits)")
print("writerows takes MANY rows:      writer.writerows([fruits])")

Output:

what you get:
a,p,p,l,e
p,e,a,r
p,l,u,m

every string was treated as a row, and a string is a sequence
of characters, so each letter landed in its own cell.

writerow  takes ONE row:        writer.writerow(fruits)
writerows takes MANY rows:      writer.writerows([fruits])
Command Prompt showing csv writerows splitting each string of a flat Python list into separate characters
writerows() on a flat list treats each string as a row of characters.

writerows() expects a list of rows. Given a list of strings it treats each string as a row, and since strings are sequences of characters, every letter gets its own cell.

The rule is short: writerow(my_list) for one row, writerows(list_of_lists) for many.

Fixing extra blank lines in the CSV file

Every other row coming out blank is a Windows line ending problem:

import csv

table = [["name", "qty"], ["apple", 3], ["pear", 5]]

# opened WITHOUT newline=""
with open("doubled.csv", "w") as f:
    csv.writer(f).writerows(table)

# opened correctly
with open("clean.csv", "w", newline="") as f:
    csv.writer(f).writerows(table)

print("raw bytes without newline='':")
print(" ", open("doubled.csv", "rb").read())
print("raw bytes with newline='':")
print(" ", open("clean.csv", "rb").read())
print()
print("lines counted by the file itself:")
print("  doubled.csv ->", len(open("doubled.csv").readlines()))
print("  clean.csv   ->", len(open("clean.csv").readlines()))
print()
print("the csv module writes its own line ending, and text mode on Windows")
print("turns that into a second one. newline='' stops the translation.")

Output:

raw bytes without newline='':
  b'name,qty\r\r\napple,3\r\r\npear,5\r\r\n'
raw bytes with newline='':
  b'name,qty\r\napple,3\r\npear,5\r\n'

lines counted by the file itself:
  doubled.csv -> 6
  clean.csv   -> 3

the csv module writes its own line ending, and text mode on Windows
turns that into a second one. newline='' stops the translation.
Command Prompt comparing CSV file bytes written with and without the newline argument showing doubled carriage returns
Without newline="" the bytes show a doubled carriage return.

The csv module writes its own line terminator. Text mode on Windows then translates the newline inside it a second time, giving the doubled carriage return visible in the bytes above.

newline="" turns that translation off and is the documented way to open a file for CSV. Include it on every open() you pass to csv.writer, on every platform, so the code behaves the same everywhere.

Write a list of dictionaries with DictWriter

When each item is a record rather than a row, DictWriter keeps the columns lined up:

import csv

people = [
    {"name": "Ada", "role": "engineer", "city": "Seattle"},
    {"name": "Grace", "role": "admiral", "city": "New York"},
]

with open("people.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "role", "city"])
    writer.writeheader()
    writer.writerows(people)

print("file contents:")
print(open("people.csv").read())

print("a key you forgot to declare is an error, not a silent drop:")
try:
    with open("bad.csv", "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["name"])
        writer.writeheader()
        writer.writerow({"name": "Ada", "role": "engineer"})
except ValueError as err:
    print("  ValueError:", err)

print()
print("pass extrasaction='ignore' if you would rather skip the extra keys.")

Output:

file contents:
name,role,city
Ada,engineer,Seattle
Grace,admiral,New York

a key you forgot to declare is an error, not a silent drop:
  ValueError: dict contains fields not in fieldnames: 'role'

pass extrasaction='ignore' if you would rather skip the extra keys.
Command Prompt showing csv DictWriter writing a list of dictionaries and raising ValueError for an undeclared field
DictWriter writes the header for you and rejects undeclared keys.

fieldnames fixes both the column order and the header line, so dictionaries whose keys are in different orders still line up correctly.

A key you did not declare raises ValueError rather than disappearing quietly. That is usually what you want, though extrasaction="ignore" is there when it is not. See also converting a dictionary to a list.

Commas and quotes inside your data

This is the reason to use the module rather than joining strings yourself:

import csv

messy = ["Smith, John", 'he said "hi"', "two\nlines", ""]

with open("quoted.csv", "w", newline="") as f:
    csv.writer(f).writerow(messy)

print("raw bytes:")
print(" ", open("quoted.csv", "rb").read())
print()
print("the comma inside a value, the quotes and even the line break")
print("were all handled without any escaping on your part.")
print()
print("this is why writing CSV by hand with ','.join() goes wrong:")
print(" ", ",".join(str(v) for v in messy).replace("\n", "<newline>"))
print("  that line now has four commas and would read back as five columns.")

Output:

raw bytes:
  b'"Smith, John","he said ""hi""","two\nlines",\r\n'

the comma inside a value, the quotes and even the line break
were all handled without any escaping on your part.

this is why writing CSV by hand with ','.join() goes wrong:
  Smith, John,he said "hi",two<newline>lines,
  that line now has four commas and would read back as five columns.
Command Prompt showing the Python csv module automatically quoting values that contain commas quotes and line breaks
Values containing commas, quotes or newlines are quoted and escaped automatically.

A value containing a comma gets wrapped in quotes, an embedded quote is doubled, and a line break survives inside a quoted field. Every CSV reader understands that convention.

Building the line with ",".join() produces a file that reads back with the wrong number of columns the moment a name contains a comma. It is a bug that hides until real data arrives.

Save a list to CSV with pandas

If pandas is already in the project, to_csv() is a one-liner:

import pandas as pd

fruits = ["apple", "pear", "plum"]
table = [["apple", 3], ["pear", 5]]

pd.DataFrame(fruits, columns=["fruit"]).to_csv("p_flat.csv", index=False)
print("flat list with pandas:")
print(open("p_flat.csv").read())

pd.DataFrame(table, columns=["name", "qty"]).to_csv("p_rows.csv", index=False)
print("list of lists with pandas:")
print(open("p_rows.csv").read())

pd.DataFrame(fruits).to_csv("p_index.csv")
print("the same thing WITHOUT index=False:")
print(open("p_index.csv").read())
print("pandas wrote the row numbers as an unnamed first column.")
print("Almost nobody wants that, so index=False is nearly always right.")

Output:

flat list with pandas:
fruit
apple
pear
plum

list of lists with pandas:
name,qty
apple,3
pear,5

the same thing WITHOUT index=False:
,0
0,apple
1,pear
2,plum

pandas wrote the row numbers as an unnamed first column.
Almost nobody wants that, so index=False is nearly always right.

index=False is the part to remember. Without it, pandas writes the row numbers as an extra unnamed first column, which then shows up as a mystery column when the file is opened again.

pandas is worth it when you also want to filter, sort or reshape before saving. For a plain list, the csv module needs no dependency at all. Converting a list to a DataFrame covers the step before this one.

Appending rows to an existing CSV

Open with "a" rather than "w":

import csv

with open("log.csv", "w", newline="") as f:
    csv.writer(f).writerow(["time", "event"])

# "a" adds to the end instead of replacing the file
for entry in (["09:00", "start"], ["09:15", "checkpoint"]):
    with open("log.csv", "a", newline="") as f:
        csv.writer(f).writerow(entry)

print("file contents:")
print(open("log.csv").read())
print('"w" truncates the file first; "a" keeps what is already there.')
print("Write the header only when the file is being created.")

Output:

file contents:
time,event
09:00,start
09:15,checkpoint

"w" truncates the file first; "a" keeps what is already there.
Write the header only when the file is being created.

"w" empties the file before writing, which quietly destroys yesterday’s data in a script that runs on a schedule. "a" adds to the end.

Write the header only when the file does not yet exist, which checking whether a file exists makes easy.

Accents and other non-ASCII characters

The default encoding is whatever the machine prefers, which is rarely UTF-8 on Windows:

import csv
import locale

print("default text encoding on this machine:", locale.getpreferredencoding(False))
print()

# chr(233) is e-acute, written this way so the source stays plain ASCII
names = ["caf" + chr(233), "na" + chr(239) + "ve", "Z" + chr(252) + "rich"]

with open("names.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerow(names)

print("saved as utf-8, raw bytes:")
print(" ", open("names.csv", "rb").read())
print()
print("read back with the right encoding:")
print(" ", ascii(open("names.csv", encoding="utf-8").read()))
print()
print("Excel often wants a byte order mark, which utf-8-sig adds:")
with open("excel.csv", "w", newline="", encoding="utf-8-sig") as f:
    csv.writer(f).writerow(names)
print(" ", open("excel.csv", "rb").read()[:12], "...")

Output:

default text encoding on this machine: cp1252

saved as utf-8, raw bytes:
  b'caf\xc3\xa9,na\xc3\xafve,Z\xc3\xbcrich\r\n'

read back with the right encoding:
  'caf\xe9,na\xefve,Z\xfcrich\n'

Excel often wants a byte order mark, which utf-8-sig adds:
  b'\xef\xbb\xbfcaf\xc3\xa9,na\xc3' ...

Passing encoding="utf-8" explicitly makes the output identical everywhere. Without it, a name with an accent can raise UnicodeEncodeError or save in a form another machine cannot read.

Excel is the exception worth knowing about. It often assumes the local code page unless the file starts with a byte order mark, and encoding="utf-8-sig" adds one.

Which method should you use?

Your dataUseResult
A flat list, one row wantedwriter.writerow(lst)a,b,c on one line
A flat list, one column wantedwriterow([x]) in a loopOne item per line
A list of listswriter.writerows(rows)One row per inner list
A list of dictionariescsv.DictWriterHeader plus aligned columns
Already using pandasdf.to_csv(path, index=False)One line, no loop
A numeric arraynp.savetxt(path, arr, delimiter=",")Formatted numbers

Related guides you may want next:

Frequently asked questions

How do I write a list to a CSV file in Python?

Open the file with newline="", make a csv.writer, and call writer.writerow(my_list) to put the list on one row.

Why does my CSV file have blank lines between rows?

The file was opened without newline="". The csv module writes its own line ending and text mode translates it again, producing a doubled carriage return.

What is the difference between writerow and writerows?

writerow() writes one row from one list. writerows() writes many rows from a list of lists. Passing a flat list to writerows() splits each string into characters.

How do I write a list to a CSV column instead of a row?

Loop over the list and call writer.writerow([item]), wrapping each item in its own list so it becomes a one-cell row.

How do I write a list of dictionaries to CSV?

Use csv.DictWriter(f, fieldnames=[...]), call writeheader(), then writerows(your_list). The fieldnames set the column order.

How do I append to a CSV instead of overwriting it?

Open it in append mode with open(path, "a", newline=""). Mode "w" empties the file first.

Should I use pandas or the csv module?

The csv module for a plain list, since it needs no dependency. pandas when you are already using DataFrames; remember index=False. The module’s behaviour is set out in the Python csv module documentation.