Blog / Coding tips

Python Write List to CSV: Header, Rows and Columns

To write a list to a CSV file in Python, open the file with open("file.csv", "w", newline="", encoding="utf-8"), create a writer with csv.writer(f), then call writer.writerow(header) for the header row and writer.writerows(rows) for a list of lists. The csv module is built into Python, so there's nothing to install, and it handles commas, quotes and line breaks inside your values for you. The newline="" part is the detail most tutorials skip, and it's the reason so many people get a blank line between every row when they open the file in Excel.

This is the companion to my post on reading a CSV file in Python without pandas, which goes the other way. Below you'll find the basic example, then the questions that come straight after it: how to write a flat list as one column, how to save a list of dictionaries, why your output has brackets in it, how to append without repeating the header, and how to make Excel show £ properly. Every snippet was run with Python 3.13, and the outputs are copied from the terminal.

Write a list of lists to CSV with a header row

The most common case is a table: a header list and a list of rows, where each row is itself a list.

# write_rows.py: write a list of lists to a CSV file with a header row
import csv

header = ["name", "module", "mark"]
rows = [
    ["Aisha Khan", "Web Development", 72],
    ["Tom Davies", "Databases", 64],
    ["Zoë Martin", "Web Development", 81],
]

with open("marks.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(header)   # one row
    writer.writerows(rows)    # many rows

print(open("marks.csv", encoding="utf-8").read(), end="")

Contents of marks.csv:

name,module,mark
Aisha Khan,Web Development,72
Tom Davies,Databases,64
Zoë Martin,Web Development,81

Each inner list became one line, with the values separated by commas. The numbers 72, 64 and 81 were turned into text automatically, because a CSV file is just text. You don't need to call str() yourself.

writerow() vs writerows()

The two method names differ by one letter, and mixing them up is the most common bug:

  • writerow(row) writes one row. Give it a single list, such as the header.
  • writerows(rows) writes many rows. Give it a list of lists (or any iterable of rows), and it calls writerow() on each one.

The three arguments to open()

  • "w" creates the file, or empties it if it already exists. Use "a" to add to the end instead (shown later).
  • newline="" stops Python from changing the line endings that the csv module writes. Leave it out and Windows users get blank lines between rows. There's a full explanation further down.
  • encoding="utf-8" makes sure names like Zoë and symbols like £ are saved correctly on every operating system. Without it, Windows may use an older encoding and crash with a UnicodeEncodeError.

The with block closes the file for you when it ends, even if an error happens halfway through. If you open the file without with, remember to call f.close(), or the last rows may never be written to disk.

Write a list to a single column, or lists as columns

If you have a flat list such as ["Leeds", "York", "Hull"] and want each value on its own line, the obvious code doesn't do what you expect. Here are three attempts side by side:

# one_column.py: a flat list of strings, written three ways
import csv

towns = ["Leeds", "York", "Hull"]

with open("wrong1.csv", "w", newline="") as f:
    csv.writer(f).writerows(towns)            # wrong: each string is split up

with open("wrong2.csv", "w", newline="") as f:
    csv.writer(f).writerow(towns)             # one row, not one column

with open("right.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["town"])                 # header
    writer.writerows([t] for t in towns)      # wrap each value in a list

for name in ["wrong1.csv", "wrong2.csv", "right.csv"]:
    print(f"--- {name}")
    print(open(name).read(), end="")

Output:

--- wrong1.csv
L,e,e,d,s
Y,o,r,k
H,u,l,l
--- wrong2.csv
Leeds,York,Hull
--- right.csv
town
Leeds
York
Hull

The first attempt split every town into letters. writerows() expects each item to be a row, and a row is a sequence of values. A string is a sequence (of characters), so "Leeds" was treated as the five values L, e, e, d and s. The second attempt wrote all three towns into one row, which is correct if you wanted a single row but not a column.

The fix is to wrap each value in its own one-item list, so that every row has exactly one value. The generator expression ([t] for t in towns) does that without building a new list in memory. If you prefer list comprehensions, [[t] for t in towns] gives the same result, and my post on Python list comprehension examples explains the syntax.

Write several lists as columns with zip()

Sometimes your data is stored the other way round: one list per column. zip() pairs up the first item of each list, then the second, and so on, which turns columns into rows:

# columns.py: several separate lists written as columns with zip()
import csv

names = ["Aisha", "Tom", "Zoë"]
marks = [72, 64, 81]
grades = ["First", "2:1", "First"]

with open("columns.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "mark", "grade"])
    writer.writerows(zip(names, marks, grades))

print(open("columns.csv", encoding="utf-8").read(), end="")

Output:

name,mark,grade
Aisha,72,First
Tom,64,2:1
Zoë,81,First

Watch out for lists of different lengths. zip() stops at the shortest list and silently drops the rest. If that's a possibility, use itertools.zip_longest(names, marks, grades, fillvalue="") instead, so missing values become empty cells rather than missing rows.

Write a list of dictionaries to CSV with DictWriter

Data from an API, a JSON file or a database usually arrives as a list of dictionaries. csv.DictWriter writes those directly, using the keys to decide which column each value goes in:

# dicts.py: a list of dictionaries to CSV with DictWriter
import csv

students = [
    {"name": "Aisha Khan", "email": "aisha@example.co.uk", "mark": 72},
    {"name": "Tom Davies", "mark": 64},                       # no email
    {"name": "Zoë Martin", "email": "zoe@example.com", "mark": 81, "notes": "resit"},
]

with open("students.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(
        f,
        fieldnames=["name", "email", "mark"],
        restval="",               # value for missing keys
        extrasaction="ignore",    # skip keys not in fieldnames
    )
    writer.writeheader()
    writer.writerows(students)

print(open("students.csv", encoding="utf-8").read(), end="")

Output:

name,email,mark
Aisha Khan,aisha@example.co.uk,72
Tom Davies,,64
Zoë Martin,zoe@example.com,81

You pass the column order with fieldnames, then writeheader() writes that list as the first line. Two options make DictWriter forgiving with messy data:

  • restval="" fills in a value when a dictionary is missing a key. Tom has no email, so he got an empty cell instead of an error.
  • extrasaction="ignore" skips keys that aren't in fieldnames. Zoë's notes key was quietly left out.

Without extrasaction="ignore", an extra key stops the program, which is actually useful while you're developing because it tells you the data isn't shaped the way you thought:

ValueError: dict contains fields not in fieldnames: 'notes'

If your dictionaries come from a JSON file, my guide to writing JSON to a file in Python with pretty print covers loading and saving the same data in JSON format. CSV is better for flat tables you want to open in a spreadsheet; JSON is better when each record has nested lists.

Fixing blank lines, brackets, commas and Excel problems

The code above produces clean files, but these are the problems people search for most when their CSV output looks wrong.

Why are there blank lines between rows?

The csv module ends every row with \r\n, which is what the CSV standard asks for. When you open a file in text mode on Windows without newline="", Python also converts each \n it writes into \r\n. The result is \r\r\n, which Excel reads as an extra empty line. I can't run Windows here, but I can force the same translation with newline="\r\n" and look at the raw bytes:

# newline.py: why newline="" matters (the blank lines in Excel bug)
import csv

rows = [["name", "mark"], ["Aisha", 72]]

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

# Simulate Windows text mode, which turns every "\n" into "\r\n"
with open("bad.csv", "w", newline="\r\n") as f:
    csv.writer(f).writerows(rows)

print("good:", open("good.csv", "rb").read())
print("bad: ", open("bad.csv", "rb").read())

Output:

good: b'name,mark\r\nAisha,72\r\n'
bad:  b'name,mark\r\r\nAisha,72\r\r\n'

The good file has one \r\n per row. The bad one has \r\r\n, and that extra \r is your blank line. Passing newline="" to open() fixes it on every operating system, so make it a habit for any CSV file you write. If you'd rather have Unix-style endings, pass lineterminator="\n" to csv.writer() as well.

Values with commas, quotes or line breaks

A comma inside a value, like an address, would normally break the columns. The csv module wraps such values in double quotes, and doubles any quotes inside them:

# quoting.py: commas, quotes and line breaks inside values
import csv, io

rows = [
    ["company", "address", "note"],
    ["Smith & Co", "1 High Street, Leeds", 'Said "call after 5"'],
    ["Patel Ltd", "22 Mill Lane\nYork", "two-line address"],
]

buf = io.StringIO()
csv.writer(buf).writerows(rows)
print(buf.getvalue(), end="")

print("--- QUOTE_NONNUMERIC")
buf = io.StringIO()
csv.writer(buf, quoting=csv.QUOTE_NONNUMERIC).writerows([["Aisha", 72, 3.5]])
print(buf.getvalue(), end="")

print("--- read it back")
for row in csv.reader(io.StringIO("company,address\nSmith & Co,\"1 High Street, Leeds\"\n")):
    print(row)

Output:

company,address,note
Smith & Co,"1 High Street, Leeds","Said ""call after 5"""
Patel Ltd,"22 Mill Lane
York",two-line address
--- QUOTE_NONNUMERIC
"Aisha",72,3.5
--- read it back
['company', 'address']
['Smith & Co', '1 High Street, Leeds']

"1 High Street, Leeds" stayed as one value, the quote marks around call after 5 became "" (the CSV way of escaping a quote), and the two-line address was quoted so the line break stays inside the cell. Reading the file back with csv.reader gives the original values. csv.QUOTE_NONNUMERIC quotes every non-number, which some older systems expect. This is exactly why you shouldn't build CSV lines with string joins.

Python write list to CSV without brackets

If your file contains lines like ['Aisha Khan', 72], you've written the list's Python representation instead of CSV. Here's the mistake, the half-fix and the right way to turn a list into a CSV string:

# to_string.py: list to a CSV string (no file), and the bracket mistake
import csv, io

row = ["Aisha Khan", "1 High Street, Leeds", 72]

print(str(row))                              # the mistake: brackets and quotes
print(",".join(str(v) for v in row))         # no brackets, but the comma breaks it

buf = io.StringIO()
csv.writer(buf, lineterminator="\n").writerow(row)
print(buf.getvalue(), end="")                # correct CSV text

Output:

['Aisha Khan', '1 High Street, Leeds', 72]
Aisha Khan,1 High Street, Leeds,72
Aisha Khan,"1 High Street, Leeds",72

str(row) gives you the brackets and quote marks. ",".join(...) removes them, but the comma in the address now looks like a column separator, so the row would have four columns instead of three. Writing to an io.StringIO buffer with csv.writer gives proper CSV text without touching the disk. That's handy for an API response, a download button in a Flask or Django app, or just printing a CSV line.

Make Excel show £ and é correctly

If you double-click a UTF-8 CSV file in Excel on Windows, £8.50 can show up as £8.50. Excel guesses the encoding, and without a hint it often guesses wrong. The hint is a byte order mark (BOM) at the start of the file, which Python adds when you use encoding="utf-8-sig":

# excel.py: make Excel show £ and é correctly
import csv

rows = [["item", "price"], ["Café lunch", "£8.50"]]

with open("plain.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerows(rows)

with open("excel.csv", "w", newline="", encoding="utf-8-sig") as f:
    csv.writer(f).writerows(rows)

print("utf-8    :", open("plain.csv", "rb").read()[:12])
print("utf-8-sig:", open("excel.csv", "rb").read()[:12])

Output:

utf-8    : b'item,price\r\n'
utf-8-sig: b'\xef\xbb\xbfitem,pric'

The only difference is the three bytes \xef\xbb\xbf at the very start. Excel sees them and reads the file as UTF-8. Python's csv.reader also handles a BOM if you open the file with encoding="utf-8-sig". Use the plain utf-8 version for files that other programs or scripts will read, and utf-8-sig for files a person will open in Excel.

Semicolons and tabs

In many European countries, Excel expects semicolons because the comma is the decimal separator. Use the delimiter argument:

# semicolon.py: a different delimiter for European Excel or tab-separated files
import csv

rows = [["name", "mark"], ["Aisha", "72,5"]]

with open("semi.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f, delimiter=";").writerows(rows)

with open("marks.tsv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f, delimiter="\t").writerows(rows)

print(open("semi.csv").read(), end="")
print(open("marks.tsv", "rb").read())

Output:

name;mark
Aisha;72,5
b'name\tmark\r\nAisha\t72,5\r\n'

With a semicolon delimiter, 72,5 doesn't need quotes. The same argument with "\t" gives a tab-separated (TSV) file.

Append a list to an existing CSV, and the pandas option

Opening a file with "a" adds new rows to the end instead of wiping it. The catch is the header: you only want it once, when the file is new. This helper checks first:

# append.py: add rows to an existing CSV, writing the header only once
import csv
from pathlib import Path

def append_row(path, row, header):
    file = Path(path)
    new_file = not file.exists() or file.stat().st_size == 0
    with file.open("a", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        if new_file:
            writer.writerow(header)
        writer.writerow(row)

header = ["date", "habit", "done"]
append_row("log.csv", ["2026-10-05", "Read 20 pages", "yes"], header)
append_row("log.csv", ["2026-10-05", "Walk", "no"], header)
append_row("log.csv", ["2026-10-06", "Read 20 pages", "yes"], header)

print(open("log.csv", encoding="utf-8").read(), end="")

Output after three calls:

date,habit,done
2026-10-05,Read 20 pages,yes
2026-10-05,Walk,no
2026-10-06,Read 20 pages,yes

Path.exists() and stat().st_size == 0 cover both a missing file and an empty one, so the header appears once at the top. This is the pattern I'd use for a simple daily log, like the habit log in my JavaScript habit tracker, if it were written in Python and saved to disk instead of the browser.

One warning: if two programs append to the same file at the same moment, rows can get mixed up. For anything with several users, use a database. My PHP PDO prepared statements guide shows the safe way to insert rows in PHP.

Writing a list to CSV with pandas

If pandas is already in your project, a DataFrame writes a CSV in one line:

# with_pandas.py: the same job with pandas, if you already use it
import pandas as pd

rows = [
    ["Aisha Khan", "Web Development", 72],
    ["Tom Davies", "Databases", 64],
]

df = pd.DataFrame(rows, columns=["name", "module", "mark"])
df.to_csv("pandas.csv", index=False)

print(open("pandas.csv").read(), end="")

Output:

name,module,mark
Aisha Khan,Web Development,72
Tom Davies,Databases,64

Don't forget index=False. Without it, pandas adds its row numbers as an unnamed first column, which is rarely what you want:

,name,mark
0,Aisha,72

For writing a few lists, I'd still use the csv module: it's built in, starts instantly, and doesn't add pandas and NumPy to your install. Pandas earns its place when you're also filtering, grouping or joining the data before you save it.

FAQ

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

Import csv, open the file with open("out.csv", "w", newline="", encoding="utf-8"), create writer = csv.writer(f), then call writer.writerow(my_list) for one row or writer.writerows(list_of_lists) for many.

How do I write a list to CSV with a header?

Call writer.writerow(["name", "mark"]) once before writer.writerows(rows). For a list of dictionaries, use csv.DictWriter(f, fieldnames=[...]) and call writeheader() first.

How do I write a Python list to a CSV in one column?

Wrap each value in a list so every row has one item: writer.writerows([v] for v in my_list). Passing the list of strings straight to writerows() splits each string into separate letters.

Why does my CSV file have blank lines between rows?

The file was opened without newline="". On Windows, Python then turns the csv module's \r\n line endings into \r\r\n, which shows up as an empty row in Excel. Add newline="" to open().

How do I write a list to CSV without brackets and quotes?

Use csv.writer instead of f.write(str(my_list)). str() gives you Python's representation, brackets included, while csv.writer writes plain comma-separated values and only adds quotes where a value contains a comma or quote.

To format numbers before you save them, such as two decimal places for prices, see my post on Python f-strings. The official csv module documentation lists every option, including custom dialects.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes