Session 20 - CSV & JSON

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

What this lesson covers

The lesson, slide by slide

1. CSV & JSON

Title

Python Fundamentals - Session 20

Save your data to a file, load it back exactly as it was

2. What you will be able to do

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:

  1. Turn a dict or list into JSON text with json.dumps, and back with json.loads.
  2. Write and read whole JSON files with json.dump and json.load.
  3. Read and write tabular data with csv.reader, csv.writer, and their Dict versions.
  1. Explain the Python-to-JSON type map (True becomes true, None becomes null).
  2. Convert CSV strings with int() and open files with newline=''.
  3. Choose CSV for flat tables and JSON for nested data.

3. What survived from Session 19 - Files & Text I/O?

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'.

4. Data That Outlives the Program

Section

Part 1

5. A file lets data survive

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.

6. Break it if you can: A file lets data survive

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.

7. Two standard formats: CSV and JSON

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.

8. By analogy: Two standard formats: CSV and JSON

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.

9. A spreadsheet vs a labeled box

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.

10. Teach it back: A spreadsheet vs a labeled box

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.

11. JSON as a String

Section

Part 2

12. json.dumps: value to text

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.

13. Restore the missing line: Dump a dict to JSON text

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.

14. Dump a dict to JSON text

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'>.

expressionvalue
json.dumps(data){"name": "Sam", "score": 90}
type(s)<class 'str'>

15. Fill in: value for Dump a dict to JSON text

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.

expressionvalue
json.dumps(data){"name": "Sam", "score": 90}
type(s)<class 'str'>

16. json.loads: text to value

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.

17. Predict the next row: Load JSON text into a dict

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

expressionvalue
type(data)<class 'dict'>
data["score"]90
data["score"] + 191

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.

18. Load JSON text into a 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.

expressionvalue
type(data)<class 'dict'>
data["score"]90
data["score"] + 191

19. What each one costs: Load JSON text into a dict

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.

expressionvalue
type(data)<class 'dict'>
data["score"]90
data["score"] + 191

20. What has to happen first: The round trip: dumps then loads

Ranking

Put in order

Put the moves of The round trip: dumps then loads into the order they have to happen.

  1. dumps flattens the dict to text
  2. loads rebuilds an equal dict
  3. Read the output

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.

21. The round trip: dumps then loads

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.

stepvalue
original{'name': 'Sam', 'score': 90, 'active': True}
text{"name": "Sam", "score": 90, "active": true}
back == originalTrue
back{'name': 'Sam', 'score': 90, 'active': True}

22. Inspect it line by line: The round trip: dumps then loads

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.

  • text is now a JSON string version of original.
  • The value that comes back equals what went in.
  • Verified by execution: True, then the rebuilt dict prints.

23. The Type Map

Section

Part 3

24. Python types become JSON types

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.

25. Watch the names change

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.

PythonJSON text
Truetrue
Falsefalse
Nonenull
["a", "b"]["a", "b"]
{...}{...}

26. Watch it run: Watch the names change

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?

  1. Step 1: Python is True
  2. Step 2: Python is False
  3. Step 3: Python is None
  4. Step 4: Python is ["a", "b"]
  5. Step 5: Python is {...}

27. JSON keys are always strings

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.

28. Where does each piece belong: Session 20 - CSV & JSON

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.

Data That Outlives the Program
A file lets data survive; Two standard formats: CSV and JSON; A spreadsheet vs a labeled box
JSON as a String
json.dumps: value to text; Dump a dict to JSON text; json.loads: text to value
The Type Map
Python types become JSON types; Watch the names change; JSON keys are always strings
s1
Data That Outlives the Program is where Session 20 - CSV & JSON puts A file lets data survive, Two standard formats: CSV and JSON, A spreadsheet vs a labeled box. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
JSON as a String is where Session 20 - CSV & JSON puts json.dumps: value to text, Dump a dict to JSON text, json.loads: text to value. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
The Type Map is where Session 20 - CSV & JSON puts Python types become JSON types, Watch the names change, JSON keys are always strings. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

29. The map runs both ways

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.

30. Finish it with less help: A JSON array becomes a list

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.

31. A JSON array becomes a 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.

expressionvalue
nums[10, 20, 30]
sum(nums)60
type(nums)<class 'list'>

32. Watch it run: A JSON array becomes a 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?

  1. Step 1: expression is nums
  2. Step 2: expression is sum(nums)
  3. Step 3: expression is type(nums)

33. JSON Files

Section

Part 4

34. dump and load work on files

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.

35. Write a dict to a file

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.

whatresult
file createdplayer.json
file contents{"name": "Sam", "score": 90}

36. Draw the shape of it: Write a dict to a file

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.

37. Read the file back

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.

expressionvalue
loaded{'name': 'Sam', 'score': 90}
loaded["name"]Sam

38. A JSON file is just text

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.

39. indent makes files readable

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.

40. Pretty-print with indent

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.

linetext
1{
2 "name": "Sam",
3 "score": 90
4}

41. Fill in: text for Pretty-print with indent

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.

linetext
1{
2"name": "Sam",
3"score": 90
4}

42. JSON Traps

Section

Part 5

43. Something is wrong here: single quotes are not JSON

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.

44. Trap: single quotes are not JSON

Trap

The 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.

inputresult
{'name': 'Sam'}JSONDecodeError
messageExpecting property name enclosed in double quotes: line 1 column 2 (char 1)

The fix

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.

inputresult
{"name": "Sam"}{'name': 'Sam'}

45. Other JSON rules to know

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.

46. Take the definitions apart: serialize vs json.decoder.JSONDecodeE…

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.

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.
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.
b1
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.
b2
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.

47. CSV Files

Section

Part 6

48. CSV is comma-separated rows

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.

49. Term to definition: Session 20 - CSV & JSON

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. serialize
  • t2. json.decoder.JSONDecodeError
  • t3. csv.writer / csv.reader
  • d1. 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.
  • d2. 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.
  • d3. writer.writerow(list) writes one row; looping over a reader yields each row as a list of strings.

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.

50. Always open CSV with newline=''

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.

51. The reader hands back one row at a time

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.

52. What has to happen first: Write rows with csv.writer

Ranking

Put in order

Put the moves of Write rows with csv.writer into the order they have to happen.

  1. Wrap the file in a csv.writer
  2. writerow takes a list, one per row
  3. The file now holds three lines

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.

53. Write rows with csv.writer

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.

rowfile line
headername,score
1Sam,90
2Ana,85

54. Watch it run: Write rows with csv.writer

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?

  1. Step 1: row is header
  2. Step 2: row is 1
  3. Step 3: row is 2

55. Restore the missing line: Read rows with csv.reader

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.

56. Read rows with csv.reader

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'.

passrow (a list)
1['name', 'score']
2['Sam', '90']
3['Ana', '85']

57. CSV Values Are Strings

Section

Part 7

58. Everything read is a string

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.

59. Something is wrong here: adding a string to 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.

60. Trap: adding a string to a number

Trap

The 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.

stepresult
total0 (int)
row[1]'90' (str)
total + row[1]TypeError: unsupported operand type(s) for +: 'int' and 'str'

The fix

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.

rowint(row[1])total
Sam,909090
Ana,8585175

61. Break it on purpose: adding a string to a number

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.

62. Finish it with less help: Skip the header, then convert

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.

63. Skip the header, then convert

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.

rowint(row[1])total
Sam,909090
Ana,8585175

64. Draw the shape of it: Skip the header, then convert

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.

65. DictReader & DictWriter

Section

Part 8

66. DictReader keys rows by header

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.

67. Restore the missing line: Read rows as dicts

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.

68. Read rows as dicts

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'}Sam90
{'name': 'Ana', 'score': '85'}Ana85

69. Inspect it line by line: Read rows as dicts

Error analysis

Annotate

Walk the callouts on Read rows as dicts. Each one is a place this is easy to get subtly wrong.

  • The first line becomes the keys, so you do not skip it manually.
  • Verified by execution: prints the name and score for each row. Values are still strings.

70. DictWriter needs fieldnames

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).

71. Write dicts as rows

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 linetext
1name,score
2Sam,90
3Ana,85

72. What each one costs: Write dicts as rows

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 linetext
1name,score
2Sam,90
3Ana,85

73. The newline Trap

Section

Part 9

74. Why newline='' matters

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.

75. Something is wrong here: forgetting newline=''

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.

76. Trap: forgetting newline=''

Trap

The 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.

passrow
1['name', 'score']
2[] (blank!)
3['Sam', '90']
4[] (blank!)

The fix

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.

passrow
1['name', 'score']
2['Sam', '90']

77. Which of these survive contact with Session 20 - CSV & JSON?

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.

Holds up
Python ships a module for each: 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'.
Breaks
JSON only allows double quotes. Python's own dict printout uses single quotes - paste that into json.loads and it fails.; You loop the scores and try to total them without converting.
sound
These are stated as this lesson states them — each one survives the edge cases Session 20 - CSV & JSON puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

78. CSV vs JSON

Section

Part 10

79. Flat and uniform? Use CSV

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.

80. Nested or varied? Use JSON

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.

81. Teach it back: Nested or varied? Use JSON

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.

82. You can use both together

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.

83. By analogy: You can use both together

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.

84. One row vs one box

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.

85. Break it if you can: One row vs one box

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.

86. Predict the next row: Nested data is a JSON job

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"]

fieldJSON shape
name"Sam"
score90
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.

87. Nested data is a JSON job

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.

fieldJSON shape
name"Sam"
score90
inventory["sword", "shield", "potion"]

88. Fill in: JSON shape for Nested data is a JSON job

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.

fieldJSON shape
name"Sam"
score90
inventory["sword", "shield", "potion"]

89. Patterns & Checks

Section

Part 11

90. Save and load JSON

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.

91. Save and load CSV

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.

92. Where this shows up: Session 20 - CSV & JSON

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).

93. Check: dumps or loads?

Check

Which call turns a dict into text?

import json

data = {"score": 90}
s = json.dumps(data)
print(type(s))
callreturns
json.dumps(data)?

Check your understanding

What does this print?

  • A. <class 'str'> (correct)
  • B. <class 'dict'>
  • C. <class 'json'>
  • D. {"score": 90}

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.

Why B tempts people
dumps returns text, not a dict. json.loads is the call that returns a dict.
Why C tempts people
There is no json type - the result of dumps is an ordinary string.
Why D tempts people
That is what print(s) would show, but the code prints type(s), which is the class.

94. Check: the type map

Check

How does None serialize?

import json

print(json.dumps({"note": None}))
PythonJSON
None?

Check your understanding

What does this print?

  • A. {"note": null} (correct)
  • B. {"note": None}
  • C. {"note": "None"}
  • D. {'note': null}

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.

Why B tempts people
None is Python's spelling; JSON writes null instead, so the output is not None.
Why C tempts people
null is a JSON literal, not the string "None" - there are no quotes around it.
Why D tempts people
JSON keys use double quotes, not single quotes, so 'note' would be invalid JSON.

95. Check: single quotes

Check

Is this valid JSON?

import json

s = "{'name': 'Sam'}"
data = json.loads(s)
inputresult
{'name': 'Sam'}?

Check your understanding

What happens?

  • A. json.decoder.JSONDecodeError (correct)
  • B. It loads fine into {'name': 'Sam'}
  • C. It returns the string unchanged
  • D. A TypeError

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.

Why B tempts people
It would load only if the quotes were double. Single quotes are not valid JSON.
Why C tempts people
loads always tries to parse; it does not hand back the raw string on failure - it raises.
Why D tempts people
The failure is a JSONDecodeError (a parsing error), not a TypeError.

96. Check: CSV value types

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?

  • A. 9090 (correct)
  • B. 180
  • C. 90
  • D. TypeError

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.

Why B tempts people
180 needs int() conversion. Without it, + joins the two strings instead of adding numbers.
Why C tempts people
Both row[1] values are added (joined), so you get more than one 90.
Why D tempts people
str + str is valid (it concatenates); adding str to int is what raises TypeError.

97. Check: dump vs dumps

Check

You want to write a dict into an open file f.

import json

with open("data.json", "w") as f:
    ______(data, f)
goalcall
write to a file?

Check your understanding

Which call goes in the blank?

  • A. json.dump (correct)
  • B. json.dumps
  • C. json.load
  • D. json.write

Answer: A

Why: json.dump(value, file) writes to a file object. The s version, dumps, returns a string instead. Verified by execution.

Why B tempts people
dumps returns a string and ignores the file - it does not write to f.
Why C tempts people
load reads a file back into a value; it does not write.
Why D tempts people
There is no json.write function; the writer is json.dump.

98. Rule out three: Check: newline=''

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.

  • A. A blank row appears between each real row
  • B. The file cannot be opened at all
  • C. Every value loses its commas
  • D. Numbers come back as ints instead of strings

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.

99. Check: newline=''

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?

  • A. A blank row appears between each real row (correct)
  • B. The file cannot be opened at all
  • C. Every value loses its commas
  • D. Numbers come back as ints instead of strings

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.

Why B tempts people
The file opens fine - the problem is extra blank rows, not a failure to open.
Why C tempts people
Commas still separate fields correctly; the doubled newline is the only issue.
Why D tempts people
CSV values are always strings regardless of newline - that is a separate rule.

100. Connect it up: Session 20 - CSV & JSON

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.

101. What you can do now

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 writeIt 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 nullthe type map (JSON spellings)
csv.reader / csv.writerrows as lists of strings
csv.DictReader / DictWriterrows 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.

Sources

  1. Python 3 docs - json module
  2. Python 3 docs - csv module
  3. Python 3 docs - json.JSONDecodeError
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 20).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108