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(","))// I4.5 · ~30 min · Intermediate
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.
You will be able to
Warm-up · Activity 1 of 7
print('"Berlin, Mitte",5'.split(","))Predict · Activity 2 of 7
import csv
import io
data = io.StringIO("name,age\nAda,36\n")
rows = list(csv.DictReader(data))
print(rows[0]["age"] + "1")Practice · Activity 3 of 7
with open("out.csv", "w", ____="", encoding="utf-8") as f:Practice · Activity 4 of 7
import csv
import io
buffer = io.StringIO()
csv.writer(buffer).writerow(["Berlin, Mitte", 5, 'say "hi"'])
print(buffer.getvalue(), end="")Practice · Activity 5 of 7
Brain teaser · Activity 6 of 7
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"])Apply · Activity 7 of 7
Check your work against this list
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.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.pyOutput
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'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.
Exercise 1 of 2
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.
Open the file with newline="" and encoding="utf-8", and loop over csv.DictReader(f).
Each row is a dict with the keys name and points, whatever the column order.
row["points"] is a string: add int(row["points"]) to the total for row["name"].
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"))
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
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.
Read the rows with csv.reader(f) from a file opened with newline="".
Write them with csv.writer(f, delimiter=";").writerows(rows); the writer adds quotes where a field contains a ;.
The number of data rows is len(rows) - 1, because the first row is the header.
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"))
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyimport 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.
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.
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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.