How to Read and Write CSV Files in Python: DictReader
Python's csv module reads rows as lists or header-keyed dicts and writes them with correct quoting. The newline rule, delimiters and converting fields.
- Course: Python study plan
- Module: Files and data formats
- Kind: Lesson
- Reading time: 13 min
- Runtime: CPython 3.11
How do you read a CSV file in Python?
Open the file with newline="" and an encoding, then pass it to csv.reader, which yields each row as a list of strings, or csv.DictReader, which yields each row as a dict keyed by the header row. The module handles quoted fields that contain commas, quotes and newlines. Every field is a string, so convert numbers with int or float.
Lesson
Comma-separated values look simple enough to split by hand, and every hand-written CSV parser breaks on the first field that contains a comma, a quote or a newline. The csv module handles the quoting rules, the dialects (tab-separated, semicolons, Excel's conventions) and the line-ending trap, and its DictReader/DictWriter map rows to dictionaries keyed by the header. This lesson covers reading rows as lists and as dicts, writing with the correct newline="", delimiters and quoting, computing over columns with the right conversions, reading from stdin and strings, and the cases CSV cannot represent.
Reading
import csv
with open("sales.csv", newline="", encoding="utf-8") as f:
reader = csv.reader(f)
header = next(reader) # the first row, as a list of strings
for row in reader: # each row: a list of strings
name, qty, price = row
Every field is a string — "3" and "2.50" must be converted. newline="" on open is required by the module: it leaves line endings to csv, which knows that a quoted field may contain one. The reader handles "a, b" (a quoted field with a comma), "say ""hi""" (a doubled quote inside a quoted field), and empty fields.
DictReader and DictWriter
with open("sales.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f): # header row → keys
total += int(row["qty"]) * float(row["price"])
with open("out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "total"])
writer.writeheader()
writer.writerow({"name": "ada", "total": 12.5})
writer.writerows(rows) # an iterable of dicts
DictReader takes the first row as field names (or fieldnames= when the file has no header) and yields one dict per row in file order; missing trailing fields are None, extra ones go under restkey. DictWriter needs fieldnames to fix the column order and raises on a dict with an unknown key (extrasaction="ignore" to drop). Rows by name survive column reordering; rows by position do not.
Writing
with open("out.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["name", "note"])
writer.writerow(["ada", "says, \"hi\""]) # quoted and escaped automatically: "says, ""hi"""
The writer quotes only fields that need it (QUOTE_MINIMAL); quoting=csv.QUOTE_ALL quotes everything, QUOTE_NONNUMERIC quotes strings and leaves numbers bare (and makes the reader convert unquoted fields to floats). Values are converted with str(), so None becomes an empty string.
Delimiters and dialects
csv.reader(f, delimiter="\t") # TSV
csv.reader(f, delimiter=";") # European Excel
csv.reader(f, dialect="excel-tab")
csv.Sniffer().sniff(sample).delimiter # guess from a sample
A dialect bundles delimiter, quote character, escaping and line terminator; excel is the default and matches what spreadsheets produce. When a file has a header you know, prefer stating the delimiter to sniffing it.
From stdin and from strings
import sys
for row in csv.reader(sys.stdin): # stdin is a file object; no newline= needed for reading
...
import io
rows = list(csv.reader(io.StringIO("a,b\n1,2\n"))) # [['a', 'b'], ['1', '2']]
text = "x,y\n"
csv.reader(text.splitlines()) # any iterable of lines works
The reader accepts any iterable of strings, which is what makes it testable with a list and usable on the judge with sys.stdin.
Computing over columns
from collections import defaultdict
totals = defaultdict(float)
for row in csv.DictReader(f):
totals[row["region"]] += int(row["qty"]) * float(row["price"])
for region in sorted(totals):
print(f"{region},{totals[region]:.2f}")
Convert at the edge (int, float, Decimal for money, date.fromisoformat for dates), validate with try/except ValueError per row (Module 5), aggregate with the collections of Module 7, and format the output with fixed decimals. A CSV of a few hundred thousand rows is fine to stream this way; larger analyses move to pandas, which reads the same files with read_csv.
Headers that vary
Real files arrive with columns in different orders, with extra columns, or with a header that differs in case or spacing from what the code expects. DictReader absorbs order changes; for the rest, normalise the header once — reader.fieldnames = [h.strip().lower() for h in reader.fieldnames] — and check that the required names are present before the loop, raising a clear error naming the missing column. A row count and a bad-row count printed at the end turn a silent partial import into a visible one.
What CSV cannot do
There is no type information — everything is text until you convert it; there is no standard for dates, booleans or nulls (an empty field could be any of them); nested data does not fit; and encodings are unspecified (a file from Excel may be Windows-1252 or UTF-8 with a byte-order mark — encoding="utf-8-sig" strips the mark). For structured, typed or nested data, JSON (next lesson) is the better exchange format; CSV's strength is that every spreadsheet and database can produce and consume it.
Pitfalls
- Splitting on commas by hand.
- Forgetting
newline=""on the file used for writing (blank lines between rows on Windows). - Treating fields as numbers without converting.
- A
DictWriterwithoutfieldnames, or with a row containing an unexpected key. - Reading an Excel export as
utf-8and gettingin the first header. - Assuming an empty field means zero.
Key takeaways
csv.reader/writerfor rows as lists,DictReader/DictWriterfor rows as dicts keyed by the header.- Open with
newline=""and an encoding; every field read is a string — convert at the edge. - The writer quotes and escapes for you;
delimiter=and dialects handle TSV and semicolons. - The reader takes any iterable of lines: files,
sys.stdin,StringIO, lists. - CSV carries no types or nesting; JSON for structured data,
pandasfor large analyses.
Common questions
Why does the csv module need newline=""?
newline="" turns off text mode's line-ending translation and leaves line endings to csv, which knows that a quoted field may contain a newline. Without it, quoted newlines can be misread, and when writing on Windows a blank line appears between rows.
What is the difference between csv.reader and csv.DictReader?
csv.reader yields each row as a list, so fields are read by position; csv.DictReader takes the first row as field names and yields each row as a dict, so fields are read by name, as in row["qty"]. Code using DictReader survives the columns being reordered.
How do I write a CSV file in Python?
Open the file with "w", newline="" and an encoding, then use csv.writer(f).writerow(row) for lists, or csv.DictWriter(f, fieldnames=...) with writeheader() and writerows(rows) for dicts. The writer quotes and escapes any field containing a comma, a quote or a newline automatically.
How do I read a semicolon or tab-separated file with Python's csv?
Pass the delimiter to the reader, csv.reader(f, delimiter=";") for European Excel exports, or use dialect="excel-tab" for tab-separated files. csv.Sniffer().sniff(sample).delimiter can guess from a sample, but when you know the format, stating the delimiter is more reliable.
Why does the first CSV header have strange characters in front?
The file was saved as UTF-8 with a byte-order mark, as Excel often does, and reading it as utf-8 leaves the mark attached to the first header. Open it with encoding="utf-8-sig", which strips the mark, so the first column name matches.
Exercises
Totals by category
Read a CSV document from standard input with csv.DictReader — columns name, category, qty, price — and total qty * price per category. Write the result as CSV to standard output with csv.writer(sys.stdout): a header category,total then one row per category in name order, totals with two decimals.
Input: a CSV document. Output: a CSV document.
name,category,qty,price
bolt,hardware,10,0.5
nut,hardware,4,0.25
glue,supplies,1,3
prints
category,total
hardware,6.00
supplies,3.00Quoting round trip
Each input line is name|note where the note may contain commas and double quotes. Write the pairs to notes_demo.csv with csv.writer (newline=""), then print the file's raw lines as repr strings to show how the writer quoted them, then read the file back with csv.reader and print name -> note per row. Delete the file at the end.
Input: lines. Output: the raw lines as reprs, then the parsed rows.
ada|says, "hi"
bob|plain
prints
'ada,"says, ""hi"""'
'bob,plain'
ada -> says, "hi"
bob -> plainIn this module: Files and data formats
- Reading and writing files — open, modes, encoding and with
- pathlib — paths as objects
- CSV — reader, writer, DictReader and the quoting rules (this lesson)
- JSON in depth — custom encoders, decoders, dataclasses and config files
- Bytes and binary data — struct, int.to_bytes, base64 and hashlib
- sqlite3 — a SQL database in the standard library
- Checkpoint — Files and data formats
← pathlib — paths as objects · JSON in depth — custom encoders, decoders, dataclasses and config files →