Skip to content
aviral gupta

// I4.5 · ~30 min · Intermediate

CSV files

After this lesson you can read and write CSV files with the csv module, including quoted fields, header rows and the semicolon files that German spreadsheets use.

Lesson 5 of 6 in I4 The standard library for real programs

You will be able to

  • Read CSV files with csv.reader and DictReader, opened with newline="", and convert the fields yourself
  • Write CSV files with csv.writer and DictWriter, and predict how fields get quoted
  • Read and write other dialects, such as delimiter=";" for German Excel
  1. Warm-up · Activity 1 of 7

    Warm-up from string methods: a CSV line with a quoted field. What does this print?

    print('"Berlin, Mitte",5'.split(","))
  2. Predict · Activity 2 of 7

    Predict before you read on. io.StringIO stands in for a file here. What does this print?

    import csv
    import io
    
    data = io.StringIO("name,age\nAda,36\n")
    rows = list(csv.DictReader(data))
    print(rows[0]["age"] + "1")
  3. Practice · Activity 3 of 7

    Fill in the argument that every CSV file should be opened with, for reading and for writing.

    with open("out.csv", "w", ____="", encoding="utf-8") as f:
    open("out.csv", "w", ="", encoding="utf-8")
  4. Practice · Activity 4 of 7

    How does the writer store these three fields? What does this print?

    import csv
    import io
    
    buffer = io.StringIO()
    csv.writer(buffer).writerow(["Berlin, Mitte", 5, 'say "hi"'])
    print(buffer.getvalue(), end="")
  5. Practice · Activity 5 of 7

    Match each piece of the csv module to what it does.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. The file has a header, and the program also passes fieldnames. What does this print?

    import csv
    import io
    
    data = io.StringIO("name,points\nAda,3\nBo,5\n")
    rows = list(csv.DictReader(data, fieldnames=["name", "points"]))
    print(len(rows), rows[0]["points"])
  7. Apply · Activity 7 of 7

    Mini-task. Write exam.csv with the header name,points and three rows: Ada 72, Bo 45 and Cy 50. Then write a program that copies every row with at least 50 points into passed.csv, with the same header. Print repr of the new file's text to check it.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Expenses in, a summary for Excel out

expenses.csv (second tab) holds four expenses; two descriptions contain a comma or quotes. The program reads the file with DictReader, prints each row and adds up the amounts per category. Then it writes summary.csv for a German spreadsheet, with semicolons and a decimal comma, and prints the raw text of that file.

main.py

import csv

totals: dict[str, float] = {}
with open("expenses.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        totals[row["category"]] = totals.get(row["category"], 0.0) + float(row["amount"])
        print(f"{row['date']}  {row['description']:<13} {row['amount']:>6}")

# A summary for a German spreadsheet: semicolons and a decimal comma.
with open("summary.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["category", "total"], delimiter=";")
    writer.writeheader()
    for category, total in sorted(totals.items()):
        writer.writerow({"category": category, "total": f"{total:.2f}".replace(".", ",")})

with open("summary.csv", newline="", encoding="utf-8") as f:
    print(repr(f.read()))

expenses.csv

date,category,description,amount
2026-09-01,food,"Bread, butter",4.20
2026-09-02,transport,Train ticket,12.90
2026-09-03,food,Coffee,3.10
2026-09-05,home,"Lamp ""Luna""",24.99

Run it with

python main.py

Output

2026-09-01  Bread, butter   4.20
2026-09-02  Train ticket   12.90
2026-09-03  Coffee          3.10
2026-09-05  Lamp "Luna"    24.99
'category;total\r\nfood;7,30\r\nhome;24,99\r\ntransport;12,90\r\n'
  • "Bread, butter" came back as one field, without its quotes.
  • The doubled quotes in "Lamp ""Luna""" were read as one quote each.
  • float(row["amount"]) converts the text before adding: the reader gives only strings.
  • With delimiter=";", the decimal commas in 7,30 need no quotes.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Add up scores

scores.csv (second tab) has the columns name and points, and a name can appear several times. Write load_scores(path), which returns a dict from each name to its total points, as int. The names contain commas, so the starter's split(",") breaks. Use the csv module; the tests also try a file whose columns come in the other order.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Open the file with newline="" and encoding="utf-8", and loop over csv.DictReader(f).

  2. Hint 2

    Each row is a dict with the keys name and points, whatever the column order.

  3. Hint 3

    row["points"] is a string: add int(row["points"]) to the total for row["name"].

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import csv


def load_scores(path: str) -> dict[str, int]:
    totals: dict[str, int] = {}
    with open(path, newline="", encoding="utf-8") as f:
        for row in csv.DictReader(f):
            name = row["name"]
            totals[name] = totals.get(name, 0) + int(row["points"])
    return totals


if __name__ == "__main__":
    print(load_scores("scores.csv"))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

def load_scores(path: str) -> dict[str, int]:
    totals: dict[str, int] = {}
    with open(path, encoding="utf-8") as f:
        next(f)  # skip the header
        for line in f:
            name, points = line.strip().split(",")
            totals[name] = totals.get(name, 0) + int(points)
    return totals


if __name__ == "__main__":
    print(load_scores("scores.csv"))

test_main.py

from main import load_scores


def write(path, text):
    with open(path, "w", newline="", encoding="utf-8") as f:
        f.write(text)


def test_given_file():
    """The given file gives 7 points for Ada and 5 for Grace"""
    got = load_scores("scores.csv")
    assert got == {"Lovelace, Ada": 7, "Hopper, Grace": 5}, f"load_scores returned {got!r}"


def test_other_column_order():
    """Columns in another order are read by name"""
    write("t_order.csv", "points,name\n2,Bo\n3,Bo\n")
    got = load_scores("t_order.csv")
    assert got == {"Bo": 5}, f"for points,name the function returned {got!r}"


def test_header_only():
    """A file with only a header gives {}"""
    write("t_empty.csv", "name,points\n")
    got = load_scores("t_empty.csv")
    assert got == {}, f"for a header-only file the function returned {got!r}"

scores.csv

name,points
"Lovelace, Ada",3
"Hopper, Grace",5
"Lovelace, Ada",4

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Export for German Excel

Write export_semicolon(src, dst). It reads the comma-separated file src and writes the same rows, header included, to dst with semicolons as the separator. It returns the number of data rows, without the header. A field that contains a semicolon must still come back as one field. The starter's replace(",", ";") also changes commas inside fields.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Read the rows with csv.reader(f) from a file opened with newline="".

  2. Hint 2

    Write them with csv.writer(f, delimiter=";").writerows(rows); the writer adds quotes where a field contains a ;.

  3. Hint 3

    The number of data rows is len(rows) - 1, because the first row is the header.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import csv


def export_semicolon(src: str, dst: str) -> int:
    with open(src, newline="", encoding="utf-8") as f:
        rows = list(csv.reader(f))
    with open(dst, "w", newline="", encoding="utf-8") as f:
        csv.writer(f, delimiter=";").writerows(rows)
    return len(rows) - 1


if __name__ == "__main__":
    with open("people.csv", "w", newline="", encoding="utf-8") as f:
        f.write('name,city\nAda,"Berlin; Mitte"\n')
    print(export_semicolon("people.csv", "people_de.csv"))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

def export_semicolon(src: str, dst: str) -> int:
    with open(src, encoding="utf-8") as f:
        text = f.read()
    with open(dst, "w", encoding="utf-8") as f:
        f.write(text.replace(",", ";"))
    return 0


if __name__ == "__main__":
    with open("people.csv", "w", newline="", encoding="utf-8") as f:
        f.write('name,city\nAda,"Berlin; Mitte"\n')
    print(export_semicolon("people.csv", "people_de.csv"))

test_main.py

import csv

from main import export_semicolon

SOURCE = 'name,city\nAda,"Berlin; Mitte"\nBo,"Köln, Süd"\n'


def export(text):
    with open("t_src.csv", "w", newline="", encoding="utf-8") as f:
        f.write(text)
    count = export_semicolon("t_src.csv", "t_dst.csv")
    with open("t_dst.csv", newline="", encoding="utf-8") as f:
        return count, f.read()


def test_count():
    """Two data rows give 2"""
    count, text = export(SOURCE)
    assert count == 2, f"export_semicolon returned {count!r}, expected 2"


def test_round_trip():
    """Read back with delimiter=';', the rows are unchanged"""
    count, text = export(SOURCE)
    with open("t_dst.csv", newline="", encoding="utf-8") as f:
        rows = list(csv.reader(f, delimiter=";"))
    want = [["name", "city"], ["Ada", "Berlin; Mitte"], ["Bo", "Köln, Süd"]]
    assert rows == want, f"reading the new file with delimiter=';' gave {rows!r}"


def test_file_text():
    """The file uses ; and \\r\\n, and quotes only the field with a ;"""
    count, text = export(SOURCE)
    want = 'name;city\r\nAda;"Berlin; Mitte"\r\nBo;Köln, Süd\r\n'
    assert text == want, f"the new file holds {text!r}"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Adding up strings

import csv
import io

data = io.StringIO("item,amount\nbread,4\ncoffee,3\n")
print(sum(row["amount"] for row in csv.DictReader(data)))

What Python prints

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Why, and the fix

The csv module gives every field as a string, and sum starts at 0, an int. Convert each value: sum(int(row["amount"]) for row in ...), or float() for amounts with decimals.

A key that is not in fieldnames

import csv
import io

writer = csv.DictWriter(io.StringIO(), fieldnames=["name"])
writer.writerow({"name": "Ada", "age": 36})

What Python prints

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

Why, and the fix

DictWriter writes exactly the columns in fieldnames and refuses a dict with other keys, so data is not dropped without notice. Add the key to fieldnames, or pass extrasaction="ignore" if you really want to leave it out.

Reading a semicolon file with the default dialect

import csv
import io

data = io.StringIO("name;points\nAda;3\n")
for row in csv.DictReader(data):
    print(row["points"])

What Python prints

KeyError: 'points'

Why, and the fix

Without delimiter=";", the whole line name;points is one field name, so there is no key points. Files from German Excel usually use semicolons: pass delimiter=";" to DictReader. print(row) shows what the keys really are.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Why not split(",")

A CSV field may contain a comma, a quote or even a line break, as long as it is in double quotes: "Berlin, Mitte". split(",") cuts such a field apart; the csv module reads it correctly. Open the file with newline="" and an encoding, then loop over csv.reader(f): each row is a list of strings. csv.DictReader(f), which you met in the I3 build, uses the first row as the field names and gives each following row as a dict. Every value is a string, so convert numbers yourself with int() or float().

Writing rows

csv.writer(f).writerow(["Ada", 36]) writes one row, and writerows writes many. csv.DictWriter(f, fieldnames=[...]) writes dicts: call writeheader() once for the header row, then writerow(row). A key that is not in fieldnames raises ValueError; a missing key is written as an empty field. The writer quotes a field only when it needs to, when it contains the delimiter, a quote or a line break, and doubles quotes inside it: say "hi" becomes "say ""hi""". Rows end with \r\n.

Dialects: other separators

The default dialect, excel, uses commas. German Excel saves CSV with semicolons, because the comma is the decimal separator there. Pass delimiter=";" to reader, writer, DictReader and DictWriter to read and write such files. Read with the wrong delimiter, each line comes back as one single field. Always open with newline="": the csv module handles line endings itself, and without it, line breaks inside quoted fields are misread and Windows gets an extra \r in every row.

Sources

Last reviewed September 29, 2026