Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~29 min· topic 7 of 12

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.

main.pywhole filepython
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.

main.pywhole filepython
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.

main.pywhole filepython
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.

main.pywhole filepython
import json

data = {"name": "Asha", "tags": ("ml", "python"), "active": True, "boss": None}
text = json.dumps(data)
print(text)
print(json.loads(text))
JSON: strings in, strings outdiagram
Rendering diagram…

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

main.pywhole filepython
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.

main.pywhole filepython
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.

main.pywhole filepython
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. 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. 2

    Add up a column

    In the second example try total += row["price"] without int(...). What error do you get? Then restore the int call.

  3. 3

    Round trip a set

    In the fourth example make the default function return list(value) instead of sorted(value) and run it a few times. Is the order guaranteed? Why does sorted make the output safe for tests?

Code & diagrams

CSV: write and read a table New tab

The comma inside Pune, India is protected by quotes in the file. Print the raw file text to see them.

Sign in to run this example in your browser.

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: 29
CSV from text, with DictWriter New tab
Sign in to run this example in your browser.

Expected output

skipping {'item': 'juice', 'price': 'x'}
total: 55
['item,price', 'tea,20', 'coffee,35']
JSON: what changes on the round trip New tab

Look at the tuple, the integer key and True/None before and after.

Sign in to run this example in your browser.

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é 175
Types JSON cannot store: use default= New tab
Sign in to run this example in your browser.

Expected output

plain dumps fails: Object of type date is not JSON serializable
{"tags": ["a", "b"], "when": "2026-10-09"}
JSON Lines and bad JSON New tab
Sign in to run this example in your browser.

Expected output

line 3: Expecting property name enclosed in double quotes (column 2)
[{'id': 1, 'ok': True}, {'id': 2, 'ok': False}]
Read a reply from an AI service New tab

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

Sign in to run this example in your browser.

Expected output

answer: Hello! How can I help?
tokens used: 19

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

main.pywhole filepython
import datetime
import json

print(json.dumps({"when": datetime.date(2026, 10, 9)}))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 4, in <module>
print(json.dumps({"when": datetime.date(2026, 10, 9)}))
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lib/python313.zip/json/__init__.py", line 231, in dumps
return _default_encoder.encode(obj)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^
File "/lib/python313.zip/json/encoder.py", line 201, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/lib/python313.zip/json/encoder.py", line 262, in iterencode
return _iterencode(o, 0)
File "/lib/python313.zip/json/encoder.py", line 181, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
f'is not JSON serializable')
TypeError: Object of type date is not JSON serializable

Break #2

CSV numbers are strings

Add the prices without converting them.

main.pywhole filepython
import csv
import io

total = 0
for row in csv.DictReader(io.StringIO("item,price\ntea,20\n")):
    total += row["price"]
print(total)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 6, in <module>
total += row["price"]
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

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.Sniffer can guess delimiters and csv.register_dialect defines named variants (tab- or semicolon-separated files). For real data analysis you will use pandas.read_csv (Phase 14), which also guesses column types, but the csv module has no dependencies and streams row by row.

  • ▸

    json.dumps is slower than specialised libraries such as orjson and msgspec for 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_hook and object_pairs_hook on json.loads let you build richer objects while parsing. parse_float=Decimal keeps money exact (Topic 1.3).

  • ▸

    Corner cases worth knowing. DictReader fills missing cells with None and collects surplus cells under the key None (a short or long row does not raise). csv.writer ends rows with \r\n by default. A CSV exported by Excel often starts with a byte-order mark, so open it with encoding="utf-8-sig" or the first header becomes \ufeffname. JSON objects with duplicate keys do not raise: the last one silently wins. Keys that are True or None become "true" and "null", and a tuple key raises TypeError.

  • ▸

    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 a json.dump fails 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.loads inside try/except json.JSONDecodeError, and consider asking the API for structured output rather than repairing text.

Remember this

  1. 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. The csv module 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. 2

    Two rules for CSV in Python. First, open the file with newline="" (and encoding="utf-8"), because the csv module handles line endings itself. Second, everything you read is a string, including numbers. Convert with int(...) or float(...) yourself. Writing is the mirror image. csv.writer(f).writerow([...]) writes a list. csv.DictWriter(f, fieldnames=[...]) writes dictionaries; call writeheader() first to write the column names.

  3. 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/false and null. Python maps them to dict, list, str, int/float, True/False and None. Every message to and from an AI API is JSON.

  4. 4

    The four functions to know: json.dumps(obj) turns Python data into a JSON string (the s means string), json.loads(text) turns a JSON string into Python data, json.dump(obj, file) writes straight to an open file, and json.load(file) reads from one. Useful options of dumps/dump: indent=2 for readable output, sort_keys=True for a stable order, and ensure_ascii=False to keep non-English letters as they are instead of \u escapes.

  5. 5

    JSON is narrower than Python. It only knows the six kinds of value above. So json.dumps raises TypeError: Object of type ... is not JSON serializable for a set, a datetime (a date and time value, Topic 7.11), a Path or your own class. Dictionary keys become strings (a key of 1 comes back as "1"). Tuples come back as lists. NaN and Infinity are not valid JSON, yet Python writes them as NaN unless you pass allow_nan=False (then it raises ValueError). To handle extra types, pass default=function to dumps. The function is called for each unknown object and returns something JSON can store (like str(value) or sorted(the_set)).

  6. 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 raises json.JSONDecodeError (a kind of ValueError) that says where the mistake is. Catch it where you parse input you do not control.

Explain it without notes

01

Why should you use the csv module instead of splitting lines on commas?

02

What is the difference between json.dump, json.dumps, json.load and json.loads?

03

Which Python values cannot be serialised by json.dumps by default, and what are your options?

04

What is JSON Lines and why is it popular for large datasets?

Practice

01

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

02

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.

03

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.writer and csv.DictReader, using newline="".

  • Done when you can explain why CSV values are strings and convert them correctly.

  • Done when you can use json.dumps, loads, dump and load and name what changes on a round trip.

  • Done when you can fix a 'not JSON serializable' error with default= or isoformat().

  • Done when you can write and read a JSON Lines file and handle a bad line.