Python CSV
last modified October 30, 2026 Python 3.15
In this article we show how to read and write CSV data with the Python
csv module.
CSV
CSV (Comma Separated Values) is a very popular import and export data format used in spreadsheets and databases. Each line in a CSV file is a data record. Each record consists of one or more fields, separated by commas.
While CSV looks simple, there are many variations: different delimiters,
line endings, and quoting characters. Fields may contain the delimiter,
quotes, or even newlines, in which case they must be quoted. The format is
described in RFC 4180, but many programs deviate from it. This is why a CSV
file should not be parsed with str.split.
Python csv module
The csv module implements classes to read and write tabular data
in CSV format. Its reader and writer objects work
with sequences of strings; the DictReader and
DictWriter classes work with dictionaries.
| Function/class | Description |
|---|---|
csv.reader | returns a reader object that iterates over the rows of a CSV file |
csv.writer | returns a writer object that writes rows into a CSV file |
csv.DictReader | reads rows into dictionaries keyed by the header |
csv.DictWriter | writes dictionaries as rows |
csv.Sniffer | deduces the format of a CSV file |
csv.register_dialect | registers a named set of format parameters |
csv.unregister_dialect | removes a registered dialect |
csv.get_dialect | returns the dialect with the given name |
csv.list_dialects | returns the names of all registered dialects |
csv.field_size_limit | returns or sets the maximum field size allowed by the parser |
Files used with the csv module should be opened with
newline=''. The module handles line endings itself; without the
argument, newlines inside quoted fields would be translated and on Windows
written files would get extra blank lines.
CSV reader
The csv.reader function returns a reader object which iterates
over the lines of the given CSV file. Each row is a list of strings.
16,6,4,12,81,6,71,6 3,5,8
The numbers.csv file contains two rows of numbers.
import csv
with open('numbers.csv', newline='') as f:
reader = csv.reader(f)
for row in reader:
print(row)
print(sum(int(e) for e in row))
We read the file and sum the numbers in each row.
with open('numbers.csv', newline='') as f:
reader = csv.reader(f)
We open the file and pass the file object to csv.reader.
for row in reader:
print(row)
print(sum(int(e) for e in row))
The reader does not convert values; every field is a string. We convert the fields to integers before summing them.
$ python read_csv.py ['16', '6', '4', '12', '81', '6', '71', '6'] 202 ['3', '5', '8'] 16
Different delimiter
The delimiter parameter sets the character that separates
fields.
pen|cup|bottle chair|book|tablet
The items.csv file separates values with the |
character.
import csv
with open('items.csv', newline='') as f:
reader = csv.reader(f, delimiter='|')
for row in reader:
print(row)
We read the file with the | delimiter.
$ python read_delimiter.py ['pen', 'cup', 'bottle'] ['chair', 'book', 'tablet']
Quoted fields
Fields that contain the delimiter, quotes, or newlines are enclosed in double quotes. A double quote inside a field is written twice.
id,name,price,note 1,Pen,1.20,blue ink 2,"Notebook, A5",3.50,"80 pages, lined" 3,"""Pro"" Marker",2.75, 4,Desk lamp,24.90,"LED warm white"
The file has a comma inside the name and the note, quotes around the word Pro, an empty field, and a note that spans two lines.
import csv
with open('products.csv', newline='') as f:
text = f.read()
for line in text.splitlines()[:3]:
print(line.split(','))
print()
with open('products.csv', newline='') as f:
reader = csv.reader(f)
for row in reader:
print(reader.line_num, row)
We compare naive splitting with the csv reader.
for line in text.splitlines()[:3]:
print(line.split(','))
Splitting on commas breaks the quoted fields into pieces and keeps the quote characters.
for row in reader:
print(reader.line_num, row)
The reader handles all the cases correctly. The line_num
attribute is the number of physical lines read so far; it jumps from 4 to 6
because the last record spans two lines.
$ python read_quoted.py ['id', 'name', 'price', 'note'] ['1', 'Pen', '1.20', 'blue ink'] ['2', '"Notebook', ' A5"', '3.50', '"80 pages', ' lined"'] 1 ['id', 'name', 'price', 'note'] 2 ['1', 'Pen', '1.20', 'blue ink'] 3 ['2', 'Notebook, A5', '3.50', '80 pages, lined'] 4 ['3', '"Pro" Marker', '2.75', ''] 6 ['4', 'Desk lamp', '24.90', 'LED\nwarm white']
DictReader
The csv.DictReader class maps each row to a dictionary. The keys
are taken from the first row of the file or passed with the
fieldnames parameter.
min, avg, max 1, 5.5, 10 2, 3.5, 5
The first line contains the keys. The values are separated with a comma and a space.
import csv
with open('values.csv', newline='') as f:
reader = csv.DictReader(f, skipinitialspace=True)
print(reader.fieldnames)
for row in reader:
print(row)
print(float(row['max']) - float(row['min']))
We read the file with DictReader.
reader = csv.DictReader(f, skipinitialspace=True)
The skipinitialspace option removes the spaces after the
delimiters. Without it, the keys would be ' avg' and
' max'.
print(float(row['max']) - float(row['min']))
We access the fields by name.
$ python dict_reader.py
['min', 'avg', 'max']
{'min': '1', 'avg': '5.5', 'max': '10'}
9.0
{'min': '2', 'avg': '3.5', 'max': '5'}
3.0
Aggregating data
CSV readers are iterators, so a large file can be processed row by row without loading it into memory.
date,region,product,units,unit_price 2026-10-01,north,pen,40,1.20 2026-10-01,south,notebook,15,3.50 2026-10-02,north,notebook,22,3.50 2026-10-02,south,pen,31,1.20 2026-10-03,north,marker,12,2.75 2026-10-03,south,marker,18,2.75
We have a small sales report.
import csv
from collections import defaultdict
revenue = defaultdict(float)
units = defaultdict(int)
with open('sales.csv', newline='') as f:
for row in csv.DictReader(f):
qty = int(row['units'])
units[row['product']] += qty
revenue[row['region']] += qty * float(row['unit_price'])
for product, qty in sorted(units.items()):
print(f'{product:<10} {qty:>4} units')
for region, total in revenue.items():
print(f'{region:<10} {total:>8.2f}')
We compute the units sold per product and the revenue per region.
for row in csv.DictReader(f):
qty = int(row['units'])
units[row['product']] += qty
revenue[row['region']] += qty * float(row['unit_price'])
The defaultdict containers start each new key at zero. For
money, the decimal module would avoid floating-point rounding
errors.
$ python aggregate.py marker 30 units notebook 37 units pen 71 units north 158.00 south 139.20
Rows to objects
Converting rows into typed objects at the point of reading keeps the rest of the program free of string conversions.
import csv
from dataclasses import dataclass
from datetime import date
@dataclass
class Sale:
date: date
region: str
product: str
units: int
unit_price: float
@property
def total(self):
return self.units * self.unit_price
def load(path):
with open(path, newline='') as f:
for row in csv.DictReader(f):
yield Sale(
date=date.fromisoformat(row['date']),
region=row['region'],
product=row['product'],
units=int(row['units']),
unit_price=float(row['unit_price']),
)
sales = list(load('sales.csv'))
best = max(sales, key=lambda s: s.total)
print(best)
print(f'{best.total:.2f}')
We load the sales into dataclass instances and find the largest sale.
def load(path):
with open(path, newline='') as f:
for row in csv.DictReader(f):
yield Sale(
date=date.fromisoformat(row['date']),
...
The generator function converts each field to its proper type: a
date, an int, and a float.
best = max(sales, key=lambda s: s.total)
With typed objects, the computation is plain Python.
$ python to_dataclass.py Sale(date=datetime.date(2026, 10, 2), region='north', product='notebook', units=22, unit_price=3.5) 77.00
CSV writer
The csv.writer function returns a writer object which converts
data into delimited strings. The writerow method writes one row,
writerows writes all rows of an iterable.
import csv
rows = [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]]
with open('numbers2.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['a', 'b', 'c', 'd', 'e', 'f'])
writer.writerows(rows)
We write a header and two rows of numbers.
writer.writerow(['a', 'b', 'c', 'd', 'e', 'f']) writer.writerows(rows)
Numbers are converted to strings with str; None is
written as an empty string.
The cat -A command shows the line endings. The default
excel dialect ends lines with \r\n, displayed as
^M$, as required by RFC 4180.
$ python write_csv.py $ cat numbers2.csv a,b,c,d,e,f 1,2,3,4,5,6 7,8,9,10,11,12 $ cat -A numbers2.csv a,b,c,d,e,f^M$ 1,2,3,4,5,6^M$ 7,8,9,10,11,12^M$
Unix line endings
The lineterminator parameter changes the line ending. The
predefined unix dialect uses \n but also quotes all
fields.
import csv
rows = [['name', 'city'], ['John Doe', 'Bratislava'], ['Roger Roe', 'New York, NY']]
with open('users.csv', 'w', newline='') as f:
writer = csv.writer(f, lineterminator='\n')
writer.writerows(rows)
We write the rows with \n line endings.
writer = csv.writer(f, lineterminator='\n')
The writer quotes the field that contains a comma automatically.
$ python write_unix.py $ cat -A users.csv name,city$ John Doe,Bratislava$ Roger Roe,"New York, NY"$
Quoting
The quoting parameter controls which fields are quoted. Python
3.12 added QUOTE_STRINGS and QUOTE_NOTNULL.
import csv
import io
row = ['Pen', 1.2, None, 'say "hi"', 3]
for name in ('QUOTE_MINIMAL', 'QUOTE_ALL', 'QUOTE_NONNUMERIC',
'QUOTE_STRINGS', 'QUOTE_NOTNULL'):
buf = io.StringIO()
csv.writer(buf, quoting=getattr(csv, name), lineterminator='\n').writerow(row)
print(f'{name:<17} {buf.getvalue()}', end='')
data = '"Pen",1.2,,"x"\n'
print(next(csv.reader(io.StringIO(data), quoting=csv.QUOTE_NONNUMERIC)))
print(next(csv.reader(io.StringIO(data), quoting=csv.QUOTE_STRINGS)))
We write the same row with all quoting modes into an in-memory buffer.
row = ['Pen', 1.2, None, 'say "hi"', 3]
The row contains strings, numbers, and None.
QUOTE_MINIMAL | quotes only fields that need it; the default |
QUOTE_ALL | quotes all fields |
QUOTE_NONNUMERIC | quotes all non-numeric fields, including None |
QUOTE_STRINGS | quotes strings; None becomes an empty unquoted field |
QUOTE_NOTNULL | quotes everything except None |
print(next(csv.reader(io.StringIO(data), quoting=csv.QUOTE_NONNUMERIC))) print(next(csv.reader(io.StringIO(data), quoting=csv.QUOTE_STRINGS)))
When reading, the two modes convert unquoted fields to float.
With QUOTE_STRINGS, an empty unquoted field is read as
None, so the round trip preserves missing values.
$ python quoting.py QUOTE_MINIMAL Pen,1.2,,"say ""hi""",3 QUOTE_ALL "Pen","1.2","","say ""hi""","3" QUOTE_NONNUMERIC "Pen",1.2,"","say ""hi""",3 QUOTE_STRINGS "Pen",1.2,,"say ""hi""",3 QUOTE_NOTNULL "Pen","1.2",,"say ""hi""","3" ['Pen', 1.2, '', 'x'] ['Pen', 1.2, None, 'x']
DictWriter
The csv.DictWriter class writes dictionaries. The
fieldnames parameter sets the columns and their order.
import csv
users = [
{'first_name': 'John', 'last_name': 'Smith', 'age': 34},
{'first_name': 'Robert', 'last_name': 'Brown'},
{'first_name': 'Julia', 'last_name': 'Griffin', 'age': 27, 'city': 'Oslo'},
]
with open('names.csv', 'w', newline='') as f:
fieldnames = ['first_name', 'last_name', 'age']
writer = csv.DictWriter(f, fieldnames=fieldnames, restval='n/a',
extrasaction='ignore', lineterminator='\n')
writer.writeheader()
writer.writerows(users)
with open('names.csv', newline='') as f:
print(f.read(), end='')
try:
with open('names2.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['first_name', 'last_name'])
writer.writerows(users)
except ValueError as e:
print(e)
We write users whose dictionaries do not all have the same keys.
writer = csv.DictWriter(f, fieldnames=fieldnames, restval='n/a',
extrasaction='ignore', lineterminator='\n')
The restval value fills in missing keys. With
extrasaction='ignore', keys that are not in
fieldnames, such as city, are skipped.
writer.writeheader()
The writeheader method writes the field names as the first
row.
writer = csv.DictWriter(f, fieldnames=['first_name', 'last_name']) writer.writerows(users)
By default, extra keys raise a ValueError, which prevents
silent data loss.
$ python dict_writer.py first_name,last_name,age John,Smith,34 Robert,Brown,n/a Julia,Griffin,27 dict contains fields not in fieldnames: 'age'
Custom dialect
A dialect is a named group of format parameters. It is registered with
csv.register_dialect and used by name in readers and writers.
import csv
csv.register_dialect('hashes', delimiter='#', quoting=csv.QUOTE_NONE,
lineterminator='\n')
print(csv.list_dialects())
with open('items3.csv', 'w', newline='') as f:
writer = csv.writer(f, dialect='hashes')
writer.writerows([('pens', 4), ('plates', 2), ('cups', 1)])
with open('items3.csv', newline='') as f:
print(f.read(), end='')
with open('items3.csv', newline='') as f:
for row in csv.reader(f, dialect='hashes'):
print(row)
We define a dialect that uses # as the delimiter.
csv.register_dialect('hashes', delimiter='#', quoting=csv.QUOTE_NONE,
lineterminator='\n')
The new dialect is added to the built-in excel,
excel-tab, and unix dialects. Using the same
dialect for writing and reading guarantees that the data round-trip.
$ python custom_dialect.py ['excel', 'excel-tab', 'unix', 'hashes'] pens#4 plates#2 cups#1 ['pens', '4'] ['plates', '2'] ['cups', '1']
Detecting the format
The csv.Sniffer class guesses the dialect of a CSV sample and
whether it has a header row.
import csv
samples = {
'semicolons': 'name;price;qty\nPen;1,20;40\nCup;3,50;15\n',
'tabs': 'name\tprice\nPen\t1.20\nCup\t3.50\n',
'pipes': 'id|name\n1|Pen\n2|Cup\n',
}
sniffer = csv.Sniffer()
for label, sample in samples.items():
dialect = sniffer.sniff(sample)
header = sniffer.has_header(sample)
rows = list(csv.reader(sample.splitlines(), dialect))
print(f'{label}: delimiter={dialect.delimiter!r} header={header}')
print(' ', rows[1])
We detect the delimiters of three samples.
dialect = sniffer.sniff(sample) header = sniffer.has_header(sample)
The sniff method returns a dialect class that can be passed to
csv.reader. In the first sample, the decimal comma in
1,20 does not confuse the sniffer. Sniffing is heuristic; for
files with a known format, set the parameters explicitly.
$ python sniffer.py semicolons: delimiter=';' header=True ['Pen', '1,20', '40'] tabs: delimiter='\t' header=True ['Pen', '1.20'] pipes: delimiter='|' header=True ['1', 'Pen']
Byte order mark
Spreadsheet programs such as Excel often save UTF-8 CSV files with a byte
order mark (BOM), the bytes EF BB BF at the start of the file.
import csv
with open('excel.csv', 'rb') as f:
print(f.read(3))
with open('excel.csv', newline='', encoding='utf-8') as f:
print(next(csv.DictReader(f)))
with open('excel.csv', newline='', encoding='utf-8-sig') as f:
print(next(csv.DictReader(f)))
We read a file that begins with a BOM.
with open('excel.csv', newline='', encoding='utf-8') as f:
print(next(csv.DictReader(f)))
With the utf-8 codec, the BOM becomes the invisible
\ufeff character at the start of the first key, so
row['name'] would raise a KeyError.
with open('excel.csv', newline='', encoding='utf-8-sig') as f:
The utf-8-sig codec removes the BOM. Since Python 3.15
(PEP 686), open uses UTF-8 by default on all platforms, but a
BOM still requires utf-8-sig.
$ python bom.py
b'\xef\xbb\xbf'
{'\ufeffname': 'Pen', 'qty': '40'}
{'name': 'Pen', 'qty': '40'}
Malformed data
By default, the reader is lenient. The strict option turns
malformed input into a csv.Error.
import csv
import io
data = 'id,name\n1,"Pen\n2,Cup\n'
try:
rows = list(csv.reader(io.StringIO(data), strict=True))
except csv.Error as e:
print('csv.Error:', e)
print(list(csv.reader(io.StringIO(data))))
data2 = 'a,b\n1,2,3\n4\n'
for row in csv.DictReader(io.StringIO(data2)):
print(row)
The first data has an unclosed quote; the second has rows with a wrong number of fields.
rows = list(csv.reader(io.StringIO(data), strict=True))
In strict mode, the unclosed quote raises an error. In the default mode, the rest of the file is silently swallowed into one field.
for row in csv.DictReader(io.StringIO(data2)):
print(row)
The DictReader stores extra fields in a list under the
None key (configurable with restkey) and fills
missing fields with None (configurable with
restval). Checking for these values detects broken rows.
$ python errors.py
csv.Error: unexpected end of data
[['id', 'name'], ['1', 'Pen\n2,Cup\n']]
{'a': '1', 'b': '2', None: ['3']}
{'a': '4', 'b': None}
Generating fake CSV data
Fake data are useful for testing. We use the faker module to
create a CSV file with one hundred users.
import csv
from faker import Faker
faker = Faker()
Faker.seed(4321)
with open('fake_users.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['id', 'first_name', 'last_name', 'occupation'])
writer.writeheader()
for i in range(1, 101):
writer.writerow({
'id': i,
'first_name': faker.first_name(),
'last_name': faker.last_name(),
'occupation': faker.job(),
})
We write the users with a DictWriter.
Faker.seed(4321)
Seeding the generator makes the output reproducible, which is useful in tests.
$ python fake_csv.py $ head -5 fake_users.csv id,first_name,last_name,occupation 1,Jonathan,Russell,Bonds trader 2,Shirley,Davidson,Clothing/textile technologist 3,Rebecca,Williams,Electronics engineer 4,Brenda,Tucker,Paediatric nurse $ wc -l fake_users.csv 101 fake_users.csv
Source
CSV File Reading and Writing - Python documentation
In this article we have worked with CSV data in Python.
Author
List all Python tutorials.