Programs constantly need to hand data to each other — a Python script to a web page, a server to an app, your program today to your program next week. The problem is that a Python dictionary lives inside a running Python program; it cannot travel. JSON solves this: it is a plain-text format for writing structured data — the same lists and dictionaries you already know — as a string that any program in any language can read. JSON is not a Python thing. It is the common language that lets a Python server, a JavaScript browser, and a database export all describe the same data and agree on what it means.
This article explains what JSON is and why it exists, the small set of rules that define it, and how Python converts between JSON text and Python data structures using the json module. It assumes you are comfortable with lists, dictionaries, and lists of dictionaries, and that you have read the file I/O material — JSON is where those skills converge.
| Level | What it covers | When to read it |
|---|---|---|
| Basic idea | What JSON is and the problem it solves; JSON's syntax rules; converting between JSON and Python with loads/dumps | Right now. |
| At a deeper level | The exact JSON-to-Python type mapping; reading and writing JSON files; the mistakes that cost the most time | Once you have converted data in both directions yourself. |
| At the deepest level | Where JSON came from and why it won; serialization as a general idea; JSON's limits and where it fits next to CSV and databases | When the mechanics feel routine and you want the bigger picture. |
1 What JSON Is and Why It Exists
JSON stands for JavaScript Object Notation. Despite the name, it has nothing to do with running JavaScript — it is simply a way of writing structured data as text. Here is a student record in JSON:
{
"name": "Ana",
"grade": 91,
"clubs": ["robotics", "chess"],
"active": true
}It should look familiar: it is nearly identical to a Python dictionary. That resemblance is why JSON is easy for you to learn — but the resemblance is not the point. The point is that this is plain text. It can be saved in a file, sent over a network, printed, emailed, stored in a database column. And any programming language — Python, JavaScript, Java, Swift, C++ — can read this text and rebuild the same structure inside itself.
That is the problem JSON solves. Data structures live inside one running program, in one language, in memory that vanishes when the program ends. To move structured data between programs — or just save it for later — you need a text form both sides agree on. JSON is that agreement.
Turning an in-memory data structure into text is called serialization (and the reverse, text back into a structure, is deserialization). The names are worth knowing because the idea is everywhere: every save file, every "export," every message between a browser and a server is serialization in some format.
You will meet JSON constantly from here on, because it has become the default serialization format of the web:
- Web APIs. When a program asks a server for data — weather, sports scores, map results — the answer almost always arrives as JSON.
- Configuration files. VS Code's settings, which you have already edited, are a JSON file (
settings.json). - Flask. When your Flask applications later send data to a browser without a full HTML page, they will send JSON.
- Datasets. Real-world data downloads are offered as CSV or JSON — and JSON handles nested structure that CSV cannot.
This is the distinction everything else in this article rests on. JSON text and a Python dictionary can look almost identical on screen, but one is a string — characters, exactly as ASCII stores them — and the other is a live data structure you can index and loop over. You cannot do data["name"] on JSON text, any more than you could on any other string. The json module exists to convert between the two, and knowing at every moment which one you are holding is the core JSON skill.
JSON's syntax was lifted from JavaScript in the early 2000s, popularised by Douglas Crockford, who described himself as having discovered it rather than invented it — the notation already existed inside JavaScript; he specified it as a standalone, language-independent format. Its main rival at the time was XML, which wraps data in verbose opening and closing tags. JSON expressed the same data in a fraction of the characters, was easier for humans to read, and mapped directly onto the objects and arrays that most languages already had. It won on simplicity.
The deeper reason it won is a design principle worth noticing: JSON is deliberately small. The entire format is six types and a handful of punctuation rules — the specification famously fits on a business card. Because it is small, every language could implement it correctly, every implementation agrees, and there is almost nothing to misunderstand. Formats, protocols, and languages that stay small travel further than ones that try to do everything.
2 The Rules: JSON's Six Types
JSON has exactly six kinds of value, and every JSON document is built from them:
| JSON type | Example | Python equivalent |
|---|---|---|
| Object | {"name": "Ana", "grade": 91} | dict |
| Array | [1, 2, 3] | list |
| String | "hello" | str |
| Number | 91 or 3.14 | int or float |
| Boolean | true / false | True / False |
| Null | null | None |
Objects and arrays can nest inside each other to any depth — an object containing an array of objects is the everyday shape of real JSON data.
JSON is stricter than Python, and the differences are exactly where errors come from. Four rules with no exceptions:
- Strings use double quotes only.
'Ana'is valid Python and invalid JSON. - Object keys must be strings.
{1: "one"}is a fine Python dict; in JSON the key must be"1". - The words are lowercase and different.
true,false,null— notTrue,False,None. - No trailing commas, no comments. A comma after the last item — legal in Python — makes JSON invalid, and there is no way to write a comment at all.
Just as important is what JSON lacks. There is no tuple, no set, no date type, and no way to store a function or an object of your own class. Serializing a Python tuple silently turns it into an array; deserializing brings it back as a list. Dates must be written as strings by agreed convention. JSON can carry data — it cannot carry Python.
A malformed JSON document raises an error that names the line and column where parsing failed — read it. The actual mistake is usually at or just before that position: a single quote, a missing comma between items, a trailing comma after the last one. For a long file, an online JSON validator or VS Code's built-in highlighting finds the break faster than eyes alone. This is the same discipline as reading a Python traceback: the message is not noise, it is the map.
JSON's strictness is a feature, not pedantry. Python accepts both quote styles as a convenience for human writers; JSON allows one so that every parser in every language agrees on every document with no ambiguity. Convenience serves the writer; strictness serves the ecosystem of readers — and a data interchange format has vastly more readers than writers.
The missing types reveal what JSON is for. A format that could serialize live Python objects — functions, class instances, open connections — would be tied to Python and dangerous to load (deserializing would mean executing someone else's code; Python's own pickle format has exactly this power and exactly this risk, which is why the documentation warns never to unpickle untrusted data). JSON's poverty of types is its safety and its portability: nothing in a JSON document can do anything. It is inert data, and every language can therefore accept it from strangers.
3 JSON in Python: loads and dumps
Python's built-in json module converts in both directions. Two functions do the work:
import json
# JSON text -> Python data (deserialize)
text = '{"name": "Ana", "grade": 91, "clubs": ["robotics", "chess"]}'
data = json.loads(text)
print(data["name"]) # Ana
print(data["clubs"][0]) # robotics
# Python data -> JSON text (serialize)
student = {"name": "Tomek", "grade": 78, "active": True}
text = json.dumps(student)
print(text) # {"name": "Tomek", "grade": 78, "active": true}The names decode as load-string and dump-string: loads() takes JSON text and returns Python data; dumps() takes Python data and returns JSON text. Notice in the last line that Python's True became JSON's true — the module handles every rule from section 2 for you.
After loads(), you are holding ordinary Python data — everything from Unit 3 applies with nothing new to learn. Real JSON is usually nested, so working with it is an exercise in reading the structure and indexing step by step:
text = '''
{
"class": "DSTP",
"students": [
{"name": "Ana", "grade": 91},
{"name": "Tomek", "grade": 78},
{"name": "Kasia", "grade": 88}
]
}
'''
data = json.loads(text)
data["students"] # a list of dicts -- a familiar shape
data["students"][1]["name"] # 'Tomek'
high = [s["name"] for s in data["students"] if s["grade"] > 80]
# ['Ana', 'Kasia']The path data["students"][1]["name"] reads outside-in: the object's students key, the list's item 1, that dict's name key. When a path fails with a KeyError or TypeError, debug it the way you debug anything: shrink it. Print data["students"], confirm its type, then add one step back at a time until the wrong assumption shows itself.
TypeError: string indices must be integers when you try data["name"] almost always means you skipped loads() — you are indexing the JSON text, not the data. The string looks like a dictionary on screen, so the eye is satisfied while the type is wrong. Check with type(data): if it says str, you have JSON that is still serialized.
A subtle and important property: serialization is not perfectly reversible. Send a Python tuple through dumps() and then loads(), and a list comes back — JSON has no tuple, so the distinction was lost in transit. An integer key becomes a string key the same way. The round trip preserves the data but flattens it to JSON's six types; anything richer must be rebuilt by the receiving program from agreed convention. Professionals call the agreed shape of a JSON document its schema — which fields exist, what types they hold — and real APIs publish their schemas precisely because JSON itself cannot enforce one.
That last point places JSON in the map you already have. A database schema is enforced by the database: a bad INSERT is refused. A JSON document's schema is enforced by nobody: a missing field or a grade of "banana" parses without complaint and fails later, wherever the code first assumes otherwise. This is the CSV lesson again at one level higher — self-describing structure, but no constraints. JSON is for moving data; databases are for keeping it. The mature pattern, which you will use in Flask, is both: data validated and stored in MySQL, serialized to JSON at the moment it needs to travel.
4 JSON Files: load and dump
To read and write JSON files, the module provides load() and dump() — the same conversions, wired directly to a file. They combine with the with open(…) pattern you already know:
import json
# Read a JSON file into Python data
with open("students.json", "r") as f:
data = json.load(f)
# Write Python data to a JSON file
with open("output.json", "w") as f:
json.dump(data, f, indent=2)One letter is the whole difference: loads/dumps work with strings, load/dump work with open files. The indent=2 argument makes the output human-readable, with nesting shown by indentation — always use it for files a person might open.
A JSON file gives a small program real persistence in about six lines — load at start, save after changes:
import json
def load_scores():
try:
with open("scores.json", "r") as f:
return json.load(f)
except FileNotFoundError:
return [] # first run: no file yet
def save_scores(scores):
with open("scores.json", "w") as f:
json.dump(scores, f, indent=2)
scores = load_scores()
scores.append({"player": "Ana", "points": 320})
save_scores(scores)The FileNotFoundError handling matters: the first time the program ever runs, there is no file to load, and starting from an empty list is the correct behaviour, not an error. This load-modify-save pattern is the standard way to give a game a save file or a settings file — and the file-I/O warning applies as ever: "w" mode erases the file the instant it opens, so a crash between open and dump can lose data. Careful programs write to a temporary file first and rename it after the write succeeds.
If the data is a flat table — every record has the same simple fields — CSV is fine and spreadsheets can open it. The moment a record contains a list (a student's clubs, a player's inventory) or nesting of any kind, CSV has no honest way to store it and JSON is the right tool. Rule of thumb: tables travel as CSV, structures travel as JSON.
A JSON save file is a complete, working persistence system — and now that you know databases, you can say precisely where it stops working. Every load reads the whole file; every save rewrites the whole file. There is no querying, so finding one record means looping over all of them in Python. There are no constraints, no types beyond the six, and no safe concurrent access. For one program, one user, and data measured in kilobytes, none of that matters and JSON files are the simplest correct choice. For many users, large data, or anything needing WHERE — the reasons databases exist — the JSON file is the wrong layer.
This completes a picture the course has been assembling since December. You now hold three ways to keep structured data, each earning its place at a different scale: JSON files for small, single-program persistence; CSV for flat tables exchanged between tools; a database for querying, constraints, and many simultaneous users. And one format for data in motion between all of them: JSON over the network, which is exactly where Flask picks up the story.
- In your own words: what problem does JSON solve? Why can a Python dictionary not simply be sent to another program directly?
- Define serialization and deserialization. Which of
json.loads()andjson.dumps()performs which? - List JSON's six types and the Python equivalent of each. Name two Python types that JSON cannot represent, and state what happens to one of them during a round trip through
dumps()andloads(). - This text is invalid JSON:
{'name': 'Ana', 'grade': 91, 'active': True,}. Find all three problems and write the corrected version. - A program prints
TypeError: string indices must be integerson the lineprint(data["name"]). What has most likely been forgotten, and what one-line check would confirm it? - Using the nested class/students example from section 3, write the expression that retrieves Kasia's grade. Then write a list comprehension producing the names of all students with a grade below 80.
- Explain the difference between
json.loads()andjson.load(). Write the code to read a fileinventory.jsoninto a variable. - In the load-modify-save pattern, why is
FileNotFoundErrorhandled by returning an empty list rather than letting the program crash? When is that the correct behaviour? - You are storing player records where each player has a name, a score, and a list of items they carry. Would you choose CSV or JSON for the save file? Justify the choice using the structure of the data.
- A database refuses a bad INSERT; a JSON document accepts a grade of
"banana"without complaint. Explain this difference using the idea of a schema, and describe how a Flask application will eventually use JSON and MySQL together so each does the job it is good at.