Session 20 of the Python Fundamentals series, covered in depth. It saves and loads structured data with two standard-library modules. From json it covers dumps and loads for strings, dump and load for files, and the Python-to-JSON type map in which True becomes true and None becomes null. From csv it covers reader and writer, DictReader and DictWriter, and the newline='' rule that applies when you open the file. It also covers when to reach for CSV, which suits flat tabular rows, and when for JSON, which suits nested data. The traps are that json.loads rejects single quotes with a JSONDecodeError, that every value a CSV reader hands back is a string that needs int() before any math, and that forgetting newline='' leaves blank rows between your data on Windows. Every snippet and error message was executed and copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 101 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 20
Save your data to a file, load it back exactly as it was
Objectives
You can already build dicts and lists in memory. This session makes them survive after the program ends - by writing them to a file and reading them back. By the end you can:
json.dumps, and back with json.loads.json.dump and json.load.csv.reader, csv.writer, and their Dict versions.True becomes true, None becomes null).int() and open files with newline=''.Warm-up
Discussion prompt
Before we open Session 20 - CSV & JSON: without looking back, what was the main idea of Session 19 - Files & Text I/O, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 19 of the Python Fundamentals series, in depth. Reading and writing real text files: open(path, mode), the with statement (context manager) that always closes the file, .read()/.readline()/for line in f/.readlines(), writing with 'w' (truncates) versus appending with 'a', stripping the trailing newline, and encoding='utf-8'.
Section
Part 1
Concept
When your program ends, every variable disappears. A file on disk stays. Saving means copying your data into a file; loading means reading it back into variables next time.
serialize — Turn an in-memory value (a dict, a list) into text you can store in a file or send over a network. Reading it back is deserializing.
Counterexample
Discussion prompt
When your program ends, every variable disappears. A file on disk stays. Saving means copying your data into a file; loading means reading it back into variables next time.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
Rather than invent your own format, use one everyone already reads. CSV stores a flat table of rows and columns. JSON stores nested structure - dicts and lists inside each other.
Python ships a module for each: csv and json. You import them; nothing to install.
Analogy
Discussion prompt
Explain Two standard formats: CSV and JSON by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Rather than invent your own format, use one everyone already reads. CSV stores a flat table of rows and columns. JSON stores nested structure - dicts and lists inside each other.
Intuition
Picture CSV as a spreadsheet: neat rows, the same columns in every row. It is perfect when every record has the same simple fields.
Picture JSON as a labeled box that can hold smaller labeled boxes and lists inside. It fits data with shape - a player who owns a list of items, a setting with sub-settings.
Explain it
Discussion prompt
Explain A spreadsheet vs a labeled box to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Picture CSV as a spreadsheet: neat rows, the same columns in every row. It is perfect when every record has the same simple fields.
Section
Part 2
Concept
json.dumps(value) takes a Python dict or list and returns a string of JSON text. The s stands for 'string'.
That string is plain text you can print, store in a variable, or write to a file yourself.
Fill the middle
Fill in the blanks
From Dump a dict to JSON text — one line has had its right-hand side removed. Put it back.
import json
data = json.dumps(data)
s = ___
print(s)
print(type(s))
Why: s is what everything below it consumes, so the wrong expression here fails later and somewhere else. json.dumps walks the dict and builds a JSON string from it.
Worked example
import json
data = {"name": "Sam", "score": 90}
s = json.dumps(data)
print(s)
print(type(s))Line 4 turns the dict into text
Why: json.dumps walks the dict and builds a JSON string from it.
The result is a str, not a dict
Why: Verified by execution: it prints the JSON text, then <class 'str'>.
| expression | value |
|---|---|
| json.dumps(data) | {"name": "Sam", "score": 90} |
| type(s) | <class 'str'> |
Comparison
Comparison matrix
From Dump a dict to JSON text: refill the value column from what you know. The rest of the table is as it appeared.
| expression | value |
|---|---|
| json.dumps(data) | {"name": "Sam", "score": 90} |
| type(s) | <class 'str'> |
Concept
json.loads(text) is the inverse: give it a JSON string, get back a real Python dict or list you can index and loop over.
Read the pair as a round trip - dumps writes text out, loads reads a value back in.
Pattern
Predict first
The table runs: type(data) | <class 'dict'> · data["score"] | 90
In Load JSON text into a dict, given the rows so far: what is the next one — the row where expression is data["score"] + 1?
Correct: data["score"] + 1 | 91
| expression | value |
|---|---|
| type(data) | <class 'dict'> |
| data["score"] | 90 |
| data["score"] + 1 | 91 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. json.loads reads the JSON text and rebuilds a Python dict.
Worked example
import json
s = '{"name": "Sam", "score": 90}'
data = json.loads(s)
print(type(data))
print(data["score"] + 1)Line 4 parses the string into a dict
Why: json.loads reads the JSON text and rebuilds a Python dict.
Now you can index it like any dict
Why: Verified by execution: type is dict, and data["score"] is the number 90, so + 1 is 91.
| expression | value |
|---|---|
| type(data) | <class 'dict'> |
| data["score"] | 90 |
| data["score"] + 1 | 91 |
Trade off
Comparison matrix
From Load JSON text into a dict: every row here is a choice with a cost. Fill the value column, then say which row you would actually pick and what you give up for it.
| expression | value |
|---|---|
| type(data) | <class 'dict'> |
| data["score"] | 90 |
| data["score"] + 1 | 91 |
Ranking
Put in order
Put the moves of The round trip: dumps then loads into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. text is now a JSON string version of original.
Worked example
import json
original = {"name": "Sam", "score": 90, "active": True}
text = json.dumps(original)
back = json.loads(text)
print(back == original)
print(back)dumps flattens the dict to text
Why: text is now a JSON string version of original.
loads rebuilds an equal dict
Why: The value that comes back equals what went in.
Read the output
Why: Verified by execution: True, then the rebuilt dict prints.
| step | value |
|---|---|
| original | {'name': 'Sam', 'score': 90, 'active': True} |
| text | {"name": "Sam", "score": 90, "active": true} |
| back == original | True |
| back | {'name': 'Sam', 'score': 90, 'active': True} |
Error analysis
Annotate
Walk the callouts on The round trip: dumps then loads. Each one is a place this is easy to get subtly wrong.
Section
Part 3
Concept
JSON has its own names for the same ideas. True becomes true, False becomes false, and None becomes null - lowercase, no Python capital letters.
Dicts become JSON objects, lists become JSON arrays, and strings/numbers stay as they are.
Worked example
import json
data = {"active": True, "note": None, "tags": ["a", "b"]}
print(json.dumps(data))True and None get JSON spellings
Why: dumps writes true and null, not Python's True and None.
Read the output
Why: Verified by execution: the booleans and null are lowercase, the list becomes a JSON array.
| Python | JSON text |
|---|---|
| True | true |
| False | false |
| None | null |
| ["a", "b"] | ["a", "b"] |
| {...} | {...} |
Pattern
Step through it
Step through Watch the names change one row at a time. What is driving the change, and what would the row after the last one be?
Concept
In a JSON object every key is a string in double quotes. Python dict keys that are strings map over cleanly; that is why {"score": 90} has quotes on score but not on 90.
Values keep their kind - numbers stay numbers, lists stay arrays - but keys are text, every time.
Sorting
Sort into buckets
These are the pieces of Session 20 - CSV & JSON, out of order. Put each one back under the part of the lesson it belongs to.
Concept
json.loads reverses it: JSON true comes back as Python True, null as None, an array as a list, an object as a dict.
So a JSON array of numbers loads straight into a list you can sum.
Faded example
Fill in the blanks
A JSON array becomes a list, with the scaffolding fading: two lines are gone now — fill both.
import json
s = '[10, 20, 30]'
nums = json.loads(s)
print(nums)
print(sum(nums))
print(type(nums))
Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. An array at the top loads into a Python list.
Worked example
import json
s = '[10, 20, 30]'
nums = json.loads(s)
print(nums)
print(sum(nums))
print(type(nums))Top-level JSON does not have to be an object
Why: An array at the top loads into a Python list.
It is a real list of ints
Why: Verified by execution: the list prints, sum is 60, type is list.
| expression | value |
|---|---|
| nums | [10, 20, 30] |
| sum(nums) | 60 |
| type(nums) | <class 'list'> |
Pattern
Step through it
Step through A JSON array becomes a list one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 4
Concept
json.dump(value, file) and json.load(file) are the file versions - no s. Instead of returning/taking a string, they write to or read from an open file object.
Remember it as: the s versions are for strings, the plain versions are for files.
Worked example
import json
data = {"name": "Sam", "score": 90}
with open("player.json", "w") as f:
json.dump(data, f)open with "w" for writing
Why: The with-block gives you a file object f and closes it for you at the end.
json.dump writes the JSON into f
Why: Verified by execution: the file player.json now holds the text below - no return value, it writes straight to disk.
| what | result |
|---|---|
| file created | player.json |
| file contents | {"name": "Sam", "score": 90} |
Blank canvas
Draw it
Draw what Write a dict to a file just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Worked example
import json
with open("player.json") as f:
loaded = json.load(f)
print(loaded)
print(loaded["name"])open with no mode = reading
Why: The default mode is read, so f gives you the file's text.
json.load parses the whole file
Why: Verified by execution: loaded is the dict again, so loaded["name"] is Sam.
| expression | value |
|---|---|
| loaded | {'name': 'Sam', 'score': 90} |
| loaded["name"] | Sam |
Concept
Open a .json file in any editor and you see the same text dumps produces. Nothing binary or secret - dump simply writes that text into the file for you.
So dump is really dumps plus a file write, and load is a file read plus loads. Same map, same rules.
Concept
By default JSON is written on one line. Pass indent=2 to dumps/dump to pretty-print it with line breaks and nesting - much easier for a human to read.
Worked example
import json
data = {"name": "Sam", "score": 90}
print(json.dumps(data, indent=2))indent=2 spreads it over lines
Why: Each key goes on its own line, indented two spaces.
Read the output
Why: Verified by execution: the three lines below print. Same data, friendlier shape.
| line | text |
|---|---|
| 1 | { |
| 2 | "name": "Sam", |
| 3 | "score": 90 |
| 4 | } |
Comparison
Comparison matrix
From Pretty-print with indent: refill the text column from what you know. The rest of the table is as it appeared.
| line | text |
|---|---|
| 1 | { |
| 2 | "name": "Sam", |
| 3 | "score": 90 |
| 4 | } |
Section
Part 5
Anomaly
Predict first
A student writes this, and it looks reasonable:
JSON only allows double quotes. Python's own dict printout uses single quotes - paste that into json.loads and it fails.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: It expects a property name in double quotes and stops at the first single quote.
Use double quotes inside the JSON string. Single-quote the Python string on the outside so the double quotes sit cleanly inside.
Why: It expects a property name in double quotes and stops at the first single quote.
Trap
JSON only allows double quotes. Python's own dict printout uses single quotes - paste that into json.loads and it fails.
import json
s = "{'name': 'Sam'}"
data = json.loads(s)
print(data)loads rejects the single quotes
Why: It expects a property name in double quotes and stops at the first single quote.
| input | result |
|---|---|
| {'name': 'Sam'} | JSONDecodeError |
| message | Expecting property name enclosed in double quotes: line 1 column 2 (char 1) |
Use double quotes inside the JSON string. Single-quote the Python string on the outside so the double quotes sit cleanly inside.
import json
s = '{"name": "Sam"}'
data = json.loads(s)
print(data)Now it parses
Why: Verified by execution: prints {'name': 'Sam'}. Rule: valid JSON always uses double quotes for keys and string values.
| input | result |
|---|---|
| {"name": "Sam"} | {'name': 'Sam'} |
Concept
No trailing comma after the last item, and every key must be a string. '{"a": 1,}' raises a JSONDecodeError too - JSON is stricter than a Python literal.
json.decoder.JSONDecodeError — The error json.loads/json.load raises when the text is not valid JSON. The message names the line, column, and character where parsing failed.
Definition probe
Sort into buckets
Every line below is part of the definition of serialize or of json.decoder.JSONDecodeError — one or the other, never both. Put each where it belongs.
Section
Part 6
Concept
A CSV file is plain text: one row per line, values separated by commas. The csv module handles the commas, quoting, and line endings so you do not parse text by hand.
csv.writer / csv.reader — writer.writerow(list) writes one row; looping over a reader yields each row as a list of strings.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
Why: These are the working definitions of serialize, json.decoder.JSONDecodeError, csv.writer / csv.reader as Session 20 - CSV & JSON uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Concept
The csv module manages line endings itself, so you must open the file with newline=''. Skip it and you can get blank rows between records (a trap we will see).
The pattern is open("data.csv", "w", newline="") for writing and open("data.csv", newline="") for reading.
Concept
A csv.reader is something you loop over, yielding one row per pass - it does not load the whole file into a list unless you ask (list(reader)).
That means you can process a huge file row by row without holding it all in memory at once.
Ranking
Put in order
Put the moves of Write rows with csv.writer into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. The writer knows how to turn a list into a comma-separated line.
Worked example
import csv
with open("scores.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "score"])
writer.writerow(["Sam", 90])
writer.writerow(["Ana", 85])Wrap the file in a csv.writer
Why: The writer knows how to turn a list into a comma-separated line.
writerow takes a list, one per row
Why: First a header row, then one row per record.
The file now holds three lines
Why: Verified by execution: scores.csv contains the rows below.
| row | file line |
|---|---|
| header | name,score |
| 1 | Sam,90 |
| 2 | Ana,85 |
Pattern
Step through it
Step through Write rows with csv.writer one row at a time. What is driving the change, and what would the row after the last one be?
Fill the middle
Fill in the blanks
From Read rows with csv.reader — one line has had its right-hand side removed. Put it back.
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
for row in reader:
print(row)
Why: reader is what everything below it consumes, so the wrong expression here fails later and somewhere else. Every pass hands you one row as a list of strings.
Worked example
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
for row in reader:
print(row)Loop the reader to get each row
Why: Every pass hands you one row as a list of strings.
Read the output
Why: Verified by execution: three lists print, header first. Note 90 comes back as the string '90'.
| pass | row (a list) |
|---|---|
| 1 | ['name', 'score'] |
| 2 | ['Sam', '90'] |
| 3 | ['Ana', '85'] |
Section
Part 7
Concept
A CSV file has no types - it is all text. So csv.reader gives you '90', not 90. To do math you must convert with int() or float() first.
This is the single most common CSV mistake: treating a numeric-looking string as a number.
Anomaly
Predict first
A student writes this, and it looks reasonable:
You loop the scores and try to total them without converting.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: total is an int and row[1] is a str, so Python refuses to add them.
Convert each value with int() before the math.
Why: total is an int and row[1] is a str, so Python refuses to add them.
Trap
You loop the scores and try to total them without converting.
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
next(reader)
total = 0
for row in reader:
total = total + row[1]
print(total)row[1] is the string '90', not 90
Why: total is an int and row[1] is a str, so Python refuses to add them.
| step | result |
|---|---|
| total | 0 (int) |
| row[1] | '90' (str) |
| total + row[1] | TypeError: unsupported operand type(s) for +: 'int' and 'str' |
Convert each value with int() before the math.
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
next(reader)
total = 0
for row in reader:
total = total + int(row[1])
print(total)int(row[1]) turns '90' into 90
Why: Verified by execution: 90 + 85 = 175 prints. Rule: convert CSV strings before doing arithmetic.
| row | int(row[1]) | total |
|---|---|---|
| Sam,90 | 90 | 90 |
| Ana,85 | 85 | 175 |
Break the constraint
Discussion prompt
The rule this trap just fixed:
Verified by execution: 90 + 85 = 175 prints. Rule: convert CSV strings before doing arithmetic.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
total is an int and row[1] is a str, so Python refuses to add them.
Faded example
Fill in the blanks
Skip the header, then convert, with the scaffolding fading: two lines are gone now — fill both.
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
next(reader)
total = 0
for row in reader:
total = total + int(row[1])
print(total)
Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. The first line is column titles, not data - pull it off before the loop.
Worked example
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
next(reader)
total = 0
for row in reader:
total = total + int(row[1])
print(total)next(reader) drops the header row
Why: The first line is column titles, not data - pull it off before the loop.
Add each converted score
Why: Verified by execution: 90 then 175, prints 175.
| row | int(row[1]) | total |
|---|---|---|
| Sam,90 | 90 | 90 |
| Ana,85 | 85 | 175 |
Blank canvas
Draw it
Draw what Skip the header, then convert just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Section
Part 8
Concept
csv.DictReader uses the first row as field names and hands each later row back as a dict. Now you write row["score"] instead of counting columns to row[1].
It is easier to read and survives column reordering - you never rely on position.
Fill the middle
Fill in the blanks
From Read rows as dicts — one line has had its right-hand side removed. Put it back.
import csv
with open("scores.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["score"])
Why: reader is what everything below it consumes, so the wrong expression here fails later and somewhere else. The first line becomes the keys, so you do not skip it manually.
Worked example
import csv
with open("scores.csv", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["name"], row["score"])DictReader reads the header itself
Why: The first line becomes the keys, so you do not skip it manually.
Access fields by name
Why: Verified by execution: prints the name and score for each row. Values are still strings.
| row (a dict) | row["name"] | row["score"] |
|---|---|---|
| {'name': 'Sam', 'score': '90'} | Sam | 90 |
| {'name': 'Ana', 'score': '85'} | Ana | 85 |
Error analysis
Annotate
Walk the callouts on Read rows as dicts. Each one is a place this is easy to get subtly wrong.
Concept
csv.DictWriter(f, fieldnames=[...]) writes dicts as rows. Call writeheader() once to write the column titles, then writerow(dict) or writerows(list_of_dicts).
Worked example
import csv
rows = [
{"name": "Sam", "score": 90},
{"name": "Ana", "score": 85},
]
with open("out.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["name", "score"])
writer.writeheader()
writer.writerows(rows)fieldnames sets the column order
Why: The writer looks up each key by name and writes columns in this order.
writeheader then writerows
Why: Verified by execution: out.csv holds the header and two data rows below.
| file line | text |
|---|---|
| 1 | name,score |
| 2 | Sam,90 |
| 3 | Ana,85 |
Trade off
Comparison matrix
From Write dicts as rows: every row here is a choice with a cost. Fill the text column, then say which row you would actually pick and what you give up for it.
| file line | text |
|---|---|
| 1 | name,score |
| 2 | Sam,90 |
| 3 | Ana,85 |
Section
Part 9
Concept
On Windows, a normal text file turns every \n into \r\n as it writes. The csv module already ends rows with \r\n, so without newline='' you get \r\r\n - and a stray blank row appears between each record.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Opening the file without newline='' on Windows.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The file holds name,score\r\r\nSam,90\r\r\n, so the reader sees an empty row after every real one.
Add newline='' to the open call.
Why: The file holds name,score\r\r\nSam,90\r\r\n, so the reader sees an empty row after every real one.
Trap
Opening the file without newline='' on Windows.
import csv
with open("bad.csv", "w") as f:
writer = csv.writer(f)
writer.writerow(["name", "score"])
writer.writerow(["Sam", 90])
with open("bad.csv") as f:
for row in csv.reader(f):
print(row)Line endings double up
Why: The file holds name,score\r\r\nSam,90\r\r\n, so the reader sees an empty row after every real one.
| pass | row |
|---|---|
| 1 | ['name', 'score'] |
| 2 | [] (blank!) |
| 3 | ['Sam', '90'] |
| 4 | [] (blank!) |
Add newline='' to the open call.
import csv
with open("good.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["name", "score"])
writer.writerow(["Sam", 90])
with open("good.csv", newline="") as f:
for row in csv.reader(f):
print(row)No more blank rows
Why: Verified by execution: only the two real rows print. Rule: always pass newline='' when you open a CSV file, for both reading and writing.
| pass | row |
|---|---|
| 1 | ['name', 'score'] |
| 2 | ['Sam', '90'] |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
csv and json. You import them; nothing to install.; Picture CSV as a spreadsheet: neat rows, the same columns in every row. It is perfect when every record has the same simple fields.; json.dumps(value) takes a Python dict or list and returns a string of JSON text. The s stands for 'string'.Section
Part 10
Concept
When every record has the same handful of simple fields - a list of scores, a table of transactions - CSV is the natural fit. It is compact and opens in any spreadsheet.
Concept
When records nest - a player who owns a list of items, settings with sub-settings, fields that differ from record to record - JSON holds that shape directly. A CSV cell cannot cleanly hold a list.
Explain it
Discussion prompt
Explain Nested or varied? Use JSON to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
When records nest - a player who owns a list of items, settings with sub-settings, fields that differ from record to record - JSON holds that shape directly. A CSV cell cannot cleanly hold a list.
Concept
The two formats are not rivals. A common pattern: read a CSV export row by row, build a list of dicts, then json.dump it when you need the nested version - or the reverse.
Analogy
Discussion prompt
Explain You can use both together by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
The two formats are not rivals. A common pattern: read a CSV export row by row, build a list of dicts, then json.dump it when you need the nested version - or the reverse.
Intuition
Ask: does one record fit in one flat row of cells? If yes, CSV. If a record needs lists or nested groups inside it, JSON.
Config files and API messages are almost always JSON; exports and datasets are often CSV.
Counterexample
Discussion prompt
Ask: does one record fit in one flat row of cells? If yes, CSV. If a record needs lists or nested groups inside it, JSON.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Config files and API messages are almost always JSON; exports and datasets are often CSV.
Pattern
Predict first
The table runs: name | "Sam" · score | 90
In Nested data is a JSON job, given the rows so far: what is the next one — the row where field is inventory?
Correct: inventory | ["sword", "shield", "potion"]
| field | JSON shape |
|---|---|
| name | "Sam" |
| score | 90 |
| inventory | ["sword", "shield", "potion"] |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. inventory is a list - JSON nests it with no trouble; a single CSV cell could not.
Worked example
import json
player = {
"name": "Sam",
"score": 90,
"inventory": ["sword", "shield", "potion"]
}
print(json.dumps(player, indent=2))A list lives inside the dict
Why: inventory is a list - JSON nests it with no trouble; a single CSV cell could not.
Read the output
Why: Verified by execution: the nested structure pretty-prints, the array indented inside the object.
| field | JSON shape |
|---|---|
| name | "Sam" |
| score | 90 |
| inventory | ["sword", "shield", "potion"] |
Comparison
Comparison matrix
From Nested data is a JSON job: refill the JSON shape column from what you know. The rest of the table is as it appeared.
| field | JSON shape |
|---|---|
| name | "Sam" |
| score | 90 |
| inventory | ["sword", "shield", "potion"] |
Section
Part 11
Pattern
1. import json
Why: It is in the standard library - nothing to install.
2. To a file: with open(name, "w") as f: json.dump(data, f)
Why: dump (no s) writes the value straight into the open file.
3. From a file: with open(name) as f: data = json.load(f)
Why: load (no s) parses the whole file back into a dict or list.
4. For a string, use dumps / loads instead
Why: The s versions swap the file for a string - same type map either way.
Pattern
1. import csv and open with newline=''
Why: The csv module owns line endings; newline='' stops the blank-row bug.
2. Write: csv.writer(f).writerow(list) per row
Why: Each list becomes one comma-separated line; use DictWriter to write dicts.
3. Read: loop csv.reader(f), one list per row
Why: Use DictReader to get dicts keyed by the header instead.
4. Convert with int()/float() before math
Why: Every value read from a CSV is a string - numbers are not automatic.
Real world
Discussion prompt
Outside this lesson: where does Session 20 - CSV & JSON actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Save and load CSV is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 20 of the Python Fundamentals series, in depth. Saving and loading structured data with two standard-library modules: json (dumps/loads for strings, dump/load for files, and the Python-to-JSON type map where True becomes true and None becomes null) and csv (reader/writer, DictReader/DictWriter, and the newline='' rule when you open the file).
Check
Which call turns a dict into text?
import json
data = {"score": 90}
s = json.dumps(data)
print(type(s))| call | returns |
|---|---|
| json.dumps(data) | ? |
Check your understanding
What does this print?
Answer: A
Why: json.dumps serializes the dict to a JSON string, so s is a str and type(s) prints <class 'str'>. Verified by execution.
Check
How does None serialize?
import json
print(json.dumps({"note": None}))| Python | JSON |
|---|---|
| None | ? |
Check your understanding
What does this print?
Answer: A
Why: In the Python-to-JSON map, None becomes the JSON literal null (lowercase), and JSON uses double quotes for keys. Verified by execution.
Check
Is this valid JSON?
import json
s = "{'name': 'Sam'}"
data = json.loads(s)| input | result |
|---|---|
| {'name': 'Sam'} | ? |
Check your understanding
What happens?
Answer: A
Why: JSON requires double quotes. With single quotes, loads raises json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes. Verified by execution.
Check
The file scores.csv has a header then Sam,90 and Ana,85.
import csv
with open("scores.csv", newline="") as f:
reader = csv.reader(f)
next(reader)
row = next(reader)
print(row[1] + row[1])| row[1] | type |
|---|---|
| '90' | str |
Check your understanding
What does this print?
Answer: A
Why: csv.reader returns strings, so row[1] is '90'. Adding two strings concatenates them, giving '9090'. To get 180 you would need int(row[1]) + int(row[1]). Verified by execution.
Check
You want to write a dict into an open file f.
import json
with open("data.json", "w") as f:
______(data, f)| goal | call |
|---|---|
| write to a file | ? |
Check your understanding
Which call goes in the blank?
Answer: A
Why: json.dump(value, file) writes to a file object. The s version, dumps, returns a string instead. Verified by execution.
Elimination
Eliminate the wrong options
What is the likely symptom when you read it back?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: Without newline='', line endings become \r\r\n on Windows, so csv.reader sees an empty [] row after each real one. Verified by execution.
Check
You write a CSV on Windows but forget newline='' in the open call.
import csv
with open("bad.csv", "w") as f:
csv.writer(f).writerow(["a", "b"])
# then read it back with csv.reader| opened with newline=''? | rows read |
|---|---|
| no | ? |
Check your understanding
What is the likely symptom when you read it back?
Answer: A
Why: Without newline='', line endings become \r\r\n on Windows, so csv.reader sees an empty [] row after each real one. Verified by execution.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Data That Outlives the Program · JSON as a String · The Type Map · JSON Files · JSON Traps · CSV Files. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You can make data outlive the program. JSON for structure with dump/load (files) and dumps/loads (strings); CSV for flat tables with reader/writer and their Dict versions.
| You write | It does |
|---|---|
| json.dumps(d) | dict to JSON string |
| json.loads(s) | JSON string to dict |
| json.dump(d, f) / json.load(f) | write / read a JSON file |
| True to true, None to null | the type map (JSON spellings) |
| csv.reader / csv.writer | rows as lists of strings |
| csv.DictReader / DictWriter | rows as dicts keyed by header |
| open(..., newline='') | the required CSV open |
| int(row[1]) | convert a CSV string before math |
Reach for CSV when a record is one flat row, JSON when it nests. Remember the three traps: single quotes break JSON, CSV values are strings, and forgetting newline='' leaves blank rows. Next session we put it to work reading and writing real data files.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.