ZetCode

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/classDescription
csv.readerreturns a reader object that iterates over the rows of a CSV file
csv.writerreturns a writer object that writes rows into a CSV file
csv.DictReaderreads rows into dictionaries keyed by the header
csv.DictWriterwrites dictionaries as rows
csv.Snifferdeduces the format of a CSV file
csv.register_dialectregisters a named set of format parameters
csv.unregister_dialectremoves a registered dialect
csv.get_dialectreturns the dialect with the given name
csv.list_dialectsreturns the names of all registered dialects
csv.field_size_limitreturns 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.

numbers.csv
16,6,4,12,81,6,71,6
3,5,8

The numbers.csv file contains two rows of numbers.

read_csv.py
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.

items.csv
pen|cup|bottle
chair|book|tablet

The items.csv file separates values with the | character.

read_delimiter.py
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.

products.csv
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.

read_quoted.py
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.

values.csv
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.

dict_reader.py
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.

sales.csv
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.

aggregate.py
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.

to_dataclass.py
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.

write_csv.py
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.

write_unix.py
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.

quoting.py
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_MINIMALquotes only fields that need it; the default
QUOTE_ALLquotes all fields
QUOTE_NONNUMERICquotes all non-numeric fields, including None
QUOTE_STRINGSquotes strings; None becomes an empty unquoted field
QUOTE_NOTNULLquotes 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.

dict_writer.py
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.

custom_dialect.py
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.

sniffer.py
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.

bom.py
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.

errors.py
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.

fake_csv.py
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.