Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Serialisation & Storage  ›  Lesson

CSV & the Dialects Nobody Agrees On

CSV 22 minReading and writing tabular text without breaking on quotes, delimiters or BOMs
You're building a piece ofQuarry — the vault
This piece — read_csv(): Reads the spreadsheet exports people hand you, BOMs, quoted commas and all.
Scenario The operations team drops last month's incident export into Quarry's yard. It was saved from a spreadsheet in Germany, with a BOM, semicolons, and a note column full of commas.
Your task
Build read_csv(text): strip a leading BOM, pick the delimiter that appears most in the first line (from , ; tab |, ties go to the comma), parse with csv.DictReader over io.StringIO(text, newline="") with skipinitialspace=True, and strip whitespace from every key. Example: read_csv('id,city\n1,"Pune, MH"\n') → [{"id": "1", "city": "Pune, MH"}].

CSV & the Dialects Nobody Agrees On

In short: CSV looks like "split each line on commas", and that line works right up until a field itself contains a comma, a quote, or a newline — all legal in real CSV, protected by wrapping the field in double quotes. split knows nothing about quotes, so it cuts there anyway, shifting every column after it, without raising an error. The standard library's csv module is a small state machine that tracks whether it's inside quotes, so a comma inside quotes stays part of the field. "Comma-separated" is also a hope, not a rule — spreadsheets from different countries use ; or \t — so CSV has dialect settings for the delimiter and quote character. Every value a reader gives you comes back as a str; CSV carries no types.

You'll build read_csv(text), the first piece of Quarry's vault: it reads the exports people actually send, not the ones you wish they sent.

A line with a quoted comma going two ways: split(",") cuts it into 4 broken fields with the city shifted into the wrong column, while csv.reader keeps the quoted comma inside one field and gets 3

Import a support-ticket export with a quoted city

A support team's export has one row per ticket. Someone typed a city with its state after a comma, so the tool that wrote the file quoted that field — exactly what quoting is for:

What does this print?
line = '3,"Pune, MH",95'
print(len(line.split(",")))

Four fields where there should be three, and nothing raised an error. The ms column now holds ' MH"'. A shifted column is worse than a crash — the program carries on and stores the wrong number under the right heading. Real exports also allow a doubled quote inside a quoted field ("she said ""hi"""), a newline inside a quoted field, \r\n line endings, and a byte-order mark glued to the first header. Each one breaks split differently; the embedded newline breaks it before you even reach the commas, because splitting on \n has already cut the record in half.

Drawing diagram…

csv.reader reads this correctly, and it wants an iterable of lines, not one string — a file, a list, or io.StringIO, which wraps a string so it behaves like one:

python

Fix the version that still uses split, so every city comes out whole:

broken — fix it

Run it first and look at the second city. Then replace the split with a csv.reader so the quoted comma stays inside its field.

Pull a column by name from a reshuffled export

Column positions are fragile — insert a column and row[2] silently means something else. csv.DictReader reads the first row as the header and hands you each later row as a dict, keyed by name:

python

Notice the blank line before 3,Oslo,40 — DictReader skips fully empty rows, which is what you want from a hand-edited file. Every value is still a str, which is easy to forget:

What does this print?
import csv, io

rows = list(csv.DictReader(io.StringIO("item,qty\nbolt,3\n")))
print(rows[0]["qty"] * 2)

Step through a small reader loop and watch row become a dict each pass, while the running total only changes once the string is converted:

step through it
1import csv, io
2 
3text = 'id,city,ms\n1,Leeds,120\n2,"Pune, MH",95\n'
4total = 0
5names = []
6for row in csv.DictReader(io.StringIO(text)):
7 ms = int(row["ms"])
8 total = total + ms
9 names.append(row["city"])
10print(total, names)

Read a spreadsheet from Germany

Much of Europe writes decimals with a comma, so spreadsheets there separate fields with ; instead. Log exporters favour tabs or |. The csv module calls one set of these choices a dialect; delimiter is the one you'll touch most:

python

How do you know which delimiter a file uses before reading it? csv.Sniffer guesses from a sample, but it can guess wrong on short or odd samples. Quarry uses a simpler rule: look at the header line and pick the candidate that appears most often in it. Headers rarely contain punctuation, so the delimiter is almost always the most common character:

python

On a tie, max returns the first candidate, so a one-column file falls back to the comma — a reasonable default.

Traps worth knowing

Writing has the mirror-image bug. Building rows with ",".join(values) breaks the moment a value contains a comma. csv.DictWriter quotes only the fields that need it, and its line ending surprises almost everyone:

What does this print?
import csv, io

buf = io.StringIO()
csv.writer(buf).writerow(["a", "b"])
print(repr(buf.getvalue()))

A leading BOM breaks row["id"] on a file that looks perfect in any editor. Strip it first: text.removeprefix(""). Then the header reads id, not 'id'.

Quick practice

What does this print?
import csv, io

rows = list(csv.reader(io.StringIO('a,"say ""hi""",b')))
print(rows[0])
What does this print?
text = "id,city\n1,\"Pune, MH\"\n2,Leeds\n"
import csv, io
print(len(list(csv.DictReader(io.StringIO(text)))))
broken — fix itKeyError

Run it, read the KeyError, then fix the DictReader call so the space doesn't become part of the key.

What does this print?
import csv, io

rows = list(csv.DictReader(io.StringIO("a;b\n1;2\n"), delimiter=";"))
print(rows[0]["a"])

🎯 Your turn

Write read_csv(text). It returns a list of dicts, one per data row, keyed by the header:

  • strip a leading "" if there is one
  • pick the delimiter: whichever of , ; \t | appears most in the first line (ties go to ,)
  • parse with csv.DictReader over io.StringIO(text, newline=""), with skipinitialspace=True
  • strip whitespace from every key

Examples:

  • read_csv("id,name\n1,Ada\n") → [{"id": "1", "name": "Ada"}]
  • read_csv('id,city\n1,"Pune, MH"\n') → [{"id": "1", "city": "Pune, MH"}]
  • read_csv("id;amount\r\n7;3,50\r\n") → [{"id": "7", "amount": "3,50"}]

Hint: the first line is text.split("\n", 1)[0]. Take it after stripping the BOM.

Press ▶ Run, try the chips in the Live App, then hit ✓ Check. Green means Quarry can read spreadsheets. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upHow many fields, really?

Write count_fields(line) returning how many fields one CSV line holds. The starter uses split and gets quoted commas wrong: use csv.reader([line]) and take its first row.

tabular-filescsv
DrillPull one column by name

Write column(text, name) returning the list of values in the column called name, in file order. Use csv.DictReader so the column is found by its header, not its position.

tabular-filesdictreader
BuildWrite CSV that other tools can read

Write write_csv(rows). rows is a list of dicts that all share the same keys. Return CSV text with a header row (the first dict's keys, in order) and one line per dict, using \n line endings. Return "" for an empty list. Let csv.DictWriter do the quoting.

tabular-filesdictwriter
BossGive the strings their types back

Write typed_rows(text, types). types maps a column name to one of "int", "float", "bool" or "str". Parse the CSV with DictReader and convert each listed column. An empty field becomes None whatever its type. For "bool", the values "true", "yes" and "1" (any case) are True and anything else is False. Columns not in types stay strings.

tabular-filestype-casting
CapstoneAn intake report with rejects

Write import_report(text). The CSV has at least the columns id and ms, and its delimiter may be , or ; (pick the one that appears more in the header line). Strip a leading BOM. For each data row, try int(row["ms"]). Return {"ok": count of good rows, "bad": [data-row numbers that failed, counting the first data row as 1], "total_ms": sum of the good ms}.

tabular-filescsvexceptions
ExplainWhy `split(",")` is not a CSV parser

A colleague says: "Our exports never contain commas in the data, so line.split(",") is fine and faster." Explain what can go wrong, why it tends to go wrong silently rather than with an error, and why "never" is a weak guarantee for a file produced by someone else's program.

read_csv.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
read_csv(text) → listParse CSV text into one dict per data row, keyed by the header.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.