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: