Topic 7.7
CSV and JSON
In one line
CSV stores tables as lines of comma-separated text and JSON stores nested data as text with objects, arrays, numbers and strings. The csv and json modules read and write them, and they are the two formats you will meet most in data and AI work.
Think of it like this
Two ways of writing down a class list. A spreadsheet printout has one row per student and the same columns in every row: name, age, city. That is CSV (comma-separated values). A family tree card for one student can hold a name, a list of subjects, and inside each subject a list of marks. That is JSON: data of any shape, nested inside itself. CSV is for flat tables, JSON is for structured records.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- CSV
- Comma-separated values: a text file where each line is a table row and values are separated by commas.
- Header
- The first row of a CSV that names the columns.
- JSON
- A text format for nested data using objects, arrays, strings, numbers, true/false and null.
- Serialise
- To turn data in memory into text or bytes that can be stored or sent.
- Deserialise (parse)
- To turn stored text or bytes back into data in memory.
- dumps / loads
- json functions that work with strings: dump to string, load from string.
- dump / load
- json functions that work with open files.
- JSON Lines
- A file with one JSON value per line, easy to append to and to stream.
Step by step
01Write a CSV file
import csv brings in the module (Topic 6.1). csv.writer turns a list into one line of the file. Notice that the value Pune, India contains a comma, so the writer wraps it in quotes automatically. newline="" is required so that the module controls line endings. The numbers 31 and 28 are written as plain text.
import csv
with open("people.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "age", "city"])
writer.writerow(["Asha", 31, "Pune, India"])
writer.writerow(["Ben", 28, "Leeds"])02Read it as dictionaries
This reads the people.csv file from the previous step. DictReader uses the first row as the keys. Every value is a string, so convert the age with int(...) before doing arithmetic. The quoted comma inside the city stays inside the value.
with open("people.csv", encoding="utf-8", newline="") as f:
for row in csv.DictReader(f):
print(row["name"], int(row["age"]) + 1, row["city"])03DictWriter
With DictWriter you list the column names once (fieldnames) and then write dictionaries. A key that is in the dictionary but not in fieldnames raises ValueError, which catches typos. A key that is in fieldnames but missing from the dictionary is written as an empty cell.
rows = [{"item": "tea", "price": 20}, {"item": "coffee", "price": 35}]
with open("menu.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["item", "price"])
writer.writeheader()
writer.writerows(rows)04JSON: strings in, strings out
dumps makes text and loads reads it back. Compare the two sides: Python's True and None become true and null, and the tuple becomes an array that returns as a list.
import json
data = {"name": "Asha", "tags": ("ml", "python"), "active": True, "boss": None}
text = json.dumps(data)
print(text)
print(json.loads(text))05Files and pretty printing
json.dump and json.load take a file. indent=2 makes the output readable for people, and ensure_ascii=False keeps letters like é as they are (remember to use UTF-8 in the file).
with open("user.json", "w", encoding="utf-8") as f:
json.dump({"name": "José", "scores": [90, 85]}, f, indent=2, ensure_ascii=False)
with open("user.json", encoding="utf-8") as f:
user = json.load(f)
print(user["name"], sum(user["scores"]))06Types JSON cannot store
A set or a datetime is not a JSON type. Give dumps a default function. It is called only for objects dumps cannot handle, and it returns a replacement value that JSON can store. isoformat() turns a date into text like 2026-10-09 (Topic 7.11). Reading it back gives you strings or lists. Converting them to the richer types is up to you.
import datetime
def convert(value):
if isinstance(value, (set, frozenset)):
return sorted(value)
if isinstance(value, (datetime.date, datetime.datetime)):
return value.isoformat()
raise TypeError(f"cannot serialise {type(value).__name__}")
record = {"when": datetime.date(2026, 10, 9), "tags": {"b", "a"}}
print(json.dumps(record, default=convert))07JSON Lines: one record per line
Write each record with json.dumps and a newline. Read by iterating lines and calling json.loads on each. A failure on one line affects only that line, which is the point.
with open("events.jsonl", "w", encoding="utf-8") as f:
for event in [{"id": 1, "ok": True}, {"id": 2, "ok": False}]:
f.write(json.dumps(event) + "\n")
with open("events.jsonl", encoding="utf-8") as f:
events = [json.loads(line) for line in f]
print(events)Try it yourself
- 1
Add a tricky value
In the first example add a row with a value that contains a double quote, such as
'He said "hi"'. Print the raw file lines and see how the quote is doubled. - 2
Add up a column
In the second example try
total += row["price"]withoutint(...). What error do you get? Then restore theintcall. - 3
Round trip a set
In the fourth example make the
defaultfunction returnlist(value)instead ofsorted(value)and run it a few times. Is the order guaranteed? Why doessortedmake the output safe for tests?
Code & diagrams
The comma inside Pune, India is protected by quotes in the file. Print the raw file text to see them.
Expected output
['name,age,city', 'Asha,31,"Pune, India"', 'Ben,28,Leeds']
{'name': 'Asha', 'age': '31', 'city': 'Pune, India'} -> next year: 32
{'name': 'Ben', 'age': '28', 'city': 'Leeds'} -> next year: 29Expected output
skipping {'item': 'juice', 'price': 'x'}
total: 55
['item,price', 'tea,20', 'coffee,35']Look at the tuple, the integer key and True/None before and after.
Expected output
{"name": "Jos\u00e9", "tags": ["ml", "python"], "active": true, "boss": null, "1": "one"}
{'name': 'José', 'tags': ['ml', 'python'], 'active': True, 'boss': None, '1': 'one'}
{
"name": "José",
"scores": [
90,
85
]
}
José 175Expected output
plain dumps fails: Object of type date is not JSON serializable
{"tags": ["a", "b"], "when": "2026-10-09"}Expected output
line 3: Expecting property name enclosed in double quotes (column 2)
[{'id': 1, 'ok': True}, {'id': 2, 'ok': False}]Real services send JSON like this. `loads` turns it into dictionaries and lists, and you walk down with keys and positions: ["choices"], then [0], then ["message"], then ["content"]. A token is a small piece of text that the model counts (Phase 16).
Expected output
answer: Hello! How can I help?
tokens used: 19Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A date in JSON
Try to dump a dictionary that holds a date.
import datetime
import json
print(json.dumps({"when": datetime.date(2026, 10, 9)}))Break #2
CSV numbers are strings
Add the prices without converting them.
import csv
import io
total = 0
for row in csv.DictReader(io.StringIO("item,price\ntea,20\n")):
total += row["price"]
print(total)Myth vs fact
Myth
A CSV file can be parsed with line.split(",").
Fact
Not safely. Quoted values may contain commas, quotes and even newlines. Use the csv module.
Myth
json.dumps and str() give the same result.
Fact
str() of a dict shows Python syntax ('a', True, None), which is not valid JSON. json.dumps writes real JSON ("a", true, null).
Myth
JSON keeps your data exactly as it was.
Fact
Tuples become lists, integer keys become strings, and sets and dates are refused. Only the six JSON kinds survive a round trip.
Myth
JSON can store any number.
Fact
Python reads huge integers fine, but other systems (JavaScript) lose precision above 2**53. Send large IDs as strings.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
csv.Sniffercan guess delimiters andcsv.register_dialectdefines named variants (tab- or semicolon-separated files). For real data analysis you will usepandas.read_csv(Phase 14), which also guesses column types, but thecsvmodule has no dependencies and streams row by row. - ▸
json.dumpsis slower than specialised libraries such asorjsonandmsgspecfor big payloads, but the standard library is correct and always present. Parsing untrusted JSON is safe from code execution (unlike pickle, Topic 7.8) but can still be abusive: validate shapes (Pydantic, Phase 15/16) and limit sizes. - ▸
object_hookandobject_pairs_hookonjson.loadslet you build richer objects while parsing.parse_float=Decimalkeeps money exact (Topic 1.3). - ▸
Corner cases worth knowing.
DictReaderfills missing cells withNoneand collects surplus cells under the keyNone(a short or long row does not raise).csv.writerends rows with\r\nby default. A CSV exported by Excel often starts with a byte-order mark, so open it withencoding="utf-8-sig"or the first header becomes\ufeffname. JSON objects with duplicate keys do not raise: the last one silently wins. Keys that areTrueorNonebecome"true"and"null", and a tuple key raisesTypeError. - ▸
Defensive note on spreadsheets: if you export user-supplied text to CSV that people will open in Excel, a cell starting with
=,+,-or@can be run as a formula (CSV injection). Prefix such cells with a single quote or a space when the data is untrusted. And if ajson.dumpfails halfway, the file is left half-written, so dump to a string first (json.dumps) and write it once, or use the temporary-file-and-replace recipe from Topic 7.4. - ▸
LLM APIs return JSON, and models sometimes produce text that only looks like JSON (trailing commas, comments, single quotes). Parse with
json.loadsinsidetry/except json.JSONDecodeError, and consider asking the API for structured output rather than repairing text.
Remember this
- 1
A CSV file is plain text. Each line is a row and the values in the row are separated by commas, often with a first line of column names (the header). It looks simple, but the details are tricky. A value can contain a comma (
"Pune, India", which must be wrapped in quotes), a quote (doubled as"") or even a line break. Never split lines on,yourself. Thecsvmodule handles all of this.csv.reader(f)gives each row as a list of strings.csv.DictReader(f)gives each row as a dictionary keyed by the header names (Topic 4.4), which is nearly always the nicer choice. - 2
Two rules for CSV in Python. First, open the file with
newline=""(andencoding="utf-8"), because thecsvmodule handles line endings itself. Second, everything you read is a string, including numbers. Convert withint(...)orfloat(...)yourself. Writing is the mirror image.csv.writer(f).writerow([...])writes a list.csv.DictWriter(f, fieldnames=[...])writes dictionaries; callwriteheader()first to write the column names. - 3
JSON (JavaScript Object Notation) is the standard way programs exchange data, because every language can read it. It has six kinds of value: objects
{"key": value}, arrays[1, 2], strings in double quotes, numbers,true/falseandnull. Python maps them todict,list,str,int/float,True/FalseandNone. Every message to and from an AI API is JSON. - 4
The four functions to know:
json.dumps(obj)turns Python data into a JSON string (thesmeans string),json.loads(text)turns a JSON string into Python data,json.dump(obj, file)writes straight to an open file, andjson.load(file)reads from one. Useful options ofdumps/dump:indent=2for readable output,sort_keys=Truefor a stable order, andensure_ascii=Falseto keep non-English letters as they are instead of\uescapes. - 5
JSON is narrower than Python. It only knows the six kinds of value above. So
json.dumpsraisesTypeError: Object of type ... is not JSON serializablefor aset, adatetime(a date and time value, Topic 7.11), aPathor your own class. Dictionary keys become strings (a key of1comes back as"1"). Tuples come back as lists.NaNandInfinityare not valid JSON, yet Python writes them asNaNunless you passallow_nan=False(then it raisesValueError). To handle extra types, passdefault=functiontodumps. The function is called for each unknown object and returns something JSON can store (likestr(value)orsorted(the_set)). - 6
For large data use JSON Lines (
.jsonl): one complete JSON object per line, no surrounding array. You can append one record at a time and read the file line by line (Topic 7.5) without loading it all. Training sets, logs and batch inputs for AI models are usually JSON Lines for exactly this reason. Bad JSON raisesjson.JSONDecodeError(a kind ofValueError) that says where the mistake is. Catch it where you parse input you do not control.
Explain it without notes
Why should you use the csv module instead of splitting lines on commas?
What is the difference between json.dump, json.dumps, json.load and json.loads?
Which Python values cannot be serialised by json.dumps by default, and what are your options?
What is JSON Lines and why is it popular for large datasets?
Practice
Write parse_scores(text) that takes CSV text with the header name,score and returns a dict that maps each name to its score as an int. Use csv.DictReader and io.StringIO (which makes a string behave like a file). Test it with "name,score\nAsha,90\nBen,75\n".
Save {"a": [1, 2, 3], "b": {"c": None}} to a file data.json with indent=2. Read it back with json.load and print data["b"]. Hint: None becomes null in the file.
Write save_jsonl(path, records), which writes each record in a list as one JSON line, and load_jsonl(path), which reads them back into a list. Test that loading returns a list equal to the one you saved. Hint: use json.dumps per record and add "\n".
Trade-offs
- ↔
CSV versus JSON. CSV is compact and opens in spreadsheets, but is flat and untyped. JSON handles nesting and types such as numbers versus strings, but is larger and cannot be opened as a table directly.
- ↔
One big JSON file versus JSON Lines. One file is simple to load as a whole. JSON Lines streams and appends, but is not a single valid JSON document and needs a loop to read.
Done when you can
Done when you can write and read a CSV file with
csv.writerandcsv.DictReader, usingnewline="".Done when you can explain why CSV values are strings and convert them correctly.
Done when you can use
json.dumps,loads,dumpandloadand name what changes on a round trip.Done when you can fix a 'not JSON serializable' error with
default=orisoformat().Done when you can write and read a JSON Lines file and handle a bad line.