JSON (JavaScript Object Notation) is the lingua franca of the web. When one program talks to another — a website asking a server for data, a mobile app saving your progress, two services exchanging a request — the payload travelling between them is almost always a JSON string: plain text, shaped as keys and values, that nearly every programming language can read and write.
For you as a Python programmer that means one job shows up constantly: take a JSON string and turn it into Python objects you can work with, and later turn your Python objects back into a JSON string to send on. Python's standard json module does both, and its whole API is two functions.
import json
data = json.loads('{"name": "Ada", "age": 36}')
print(data['name']) # Ada
reply = json.dumps({'ok': True})
print(reply) # {"ok": true}
json.loads turns a JSON string into Python objects (a dict here). json.dumps does the reverse — a Python dict back into a JSON string.
flowchart LR S["JSON string"] -->|json.loads| O["Python dict / list"] O -->|json.dumps| S style O fill:#3776ab,color:#fff
The two functions, and why their names look odd
Both names end in s, and that s is the whole point: it stands for string. json.loads — load-string — takes a JSON string apart and hands you back live Python objects. json.dumps — dump-string — packs Python objects up into a JSON string. (Without the s, json.load and json.dump read and write files instead; we avoid those here because the browser sandbox has no disk.)
The conversion is structural: a JSON object {} becomes a Python dict, a JSON array [] becomes a Python list, and the scalar values map across too. Once parsed, the result is ordinary Python, so you reach into it with the indexing you already know — including the nested lists and dicts that real data loves to use:
import json
data = json.loads('{"a": 1, "b": [10, 20]}')
print(data['b'][1]) # 20
print(len(data)) # 2
import json
data = json.loads('{"name": "Ada", "age": 36}')
print(data['name'])
print(data['age'])
reply = json.dumps({'ok': True, 'count': 3})
print(reply)
Mind the type translations
JSON and Python agree on the shape of containers but spell some values differently, and a few types live on only one side. Python's True, False, and None become JSON's lowercase true, false, and null — and come back as True, False, None when you parse them again. Numbers survive intact: a JSON 36 round-trips to a Python int, never to the string "36". The one rule that catches newcomers is that JSON keys are always strings, so json.dumps({1: 'one'}) is forced to write {"1": "one"}.
The practical upshot: a round-trip preserves your data's structure exactly, but never assume a non-string key stayed non-string. Read values out with the type you expect, and convert when it matters — 36.0, for instance, arrives as a Python float, since JSON has no integer type.
flowchart LR
P1["Python dict {}"] -->|dumps| J1["JSON object"]
J1 -->|loads| P1
P2["Python list []"] -->|dumps| J2["JSON array"]
J2 -->|loads| P2
P3["True / False / None"] -->|dumps| J3["true / false / null"]
J3 -->|loads| P3
style P1 fill:#3776ab,color:#fff
style J1 fill:#15803d,color:#fff
import json
record = {"active": True, "tags": ["x", "y"], "owner": None}
text = json.dumps(record)
print(text)
back = json.loads(text)
print(back["active"], type(back["active"]).__name__)
print(back["tags"][1])
Real data is deeply nested
The toy examples above are flat on purpose, but the JSON you meet in the wild is rarely one level deep. A typical API reply nests lists inside dicts inside dicts — a user object holding an address object holding a list of tags. You read it the same way you read any nested Python: chain your indexing one level at a time, reply['user']['address']['city'], and when you are not sure a level exists, .get() each step so a missing branch returns None instead of crashing the program.
A habit worth forming while you learn: parse the string once, then print the resulting object to see its shape before you write the code that digs into it. You cannot index confidently into data whose structure you have not looked at.
The two ways parsing fails
Real JSON comes from messy places — a typo in a config file, a stray trailing comma, a server that sent back an HTML error page instead of the JSON you expected. When the text you hand to json.loads is not valid JSON, Python raises json.JSONDecodeError rather than returning something half-parsed; nothing silently slips through. The second failure is sneakier: the JSON parsed fine, but it has no key with the name you asked for, so indexing it raises a KeyError.
Both are normal events in code that talks to the outside world, not bugs in your program. The defensive shape is to wrap the parse in try/except for malformed input, and to fetch optional keys with .get('field') — which returns None for a missing key instead of crashing. You will write both guards in the exercises.
flowchart TD
A["json.loads(text)"] --> B{"Valid JSON?"}
B -->|no| C["raises JSONDecodeError"]
B -->|yes| D["returns dict / list"]
D --> E{"key present?"}
E -->|no| F["raises KeyError"]
E -->|yes| G["the value"]
style C fill:#b91c1c,color:#fff
style F fill:#b45309,color:#fff
style G fill:#15803d,color:#fff
import json
def safe_age(payload):
try:
data = json.loads(payload)
except json.JSONDecodeError:
return None
return data.get("age")
print(safe_age('{"name": "Ada", "age": 36}')) # 36
print(safe_age('not json at all')) # None
print(safe_age('{"name": "Bo"}')) # None — no age key
Write parse_age(payload) that takes a JSON string like '{"name": "Ada", "age": 36}' and returns the age value as an int.
import json
def parse_age(payload):
# json.loads(payload) gives a dict; return its 'age'
pass
Parse the string with json.loads(payload).
Then index ['age'] on the resulting dict.
import json
def parse_age(payload):
return json.loads(payload)['age']
What does this print? The JSON has a list nested under key 'b'.
import json
data = json.loads('{"a": 1, "b": [10, 20]}')
print(data['b'][1])
print(len(data))
data['b'] is the list [10, 20]; index 1 is the second item.
len(data) counts the top-level keys: 'a' and 'b'.
Now go the other way. Write to_json(obj) that takes a Python dict and returns its JSON string — the reverse of parsing. So to_json({'ok': True}) returns '{"ok": true}'.
import json
def to_json(obj):
# json.dumps turns a Python object into a JSON string
pass
The function you want is json.dumps — d for dump, s for string.
json.dumps produces exactly one line with ', ' and ': ' between items by default.
import json
def to_json(obj):
return json.dumps(obj)
This helper crashes with a KeyError whenever the JSON has no email field. Fix it so a missing email returns None instead of crashing. (Hint: data.get('email') returns None for an absent key.)
import json
def get_email(payload):
data = json.loads(payload)
return data['email'] # crashes if 'email' is absent
Indexing data['email'] throws when the key is missing.
Use data.get('email') — it returns None instead of raising.
import json
def get_email(payload):
data = json.loads(payload)
return data.get('email')
Round-trip. This dict is turned into JSON and parsed straight back. Predict the last two lines — and notice the types that come back.
import json
info = {"count": 3, "on": True, "name": "Ada"}
roundtrip = json.loads(json.dumps(info))
print(roundtrip["count"] + 1)
print(roundtrip["on"])
dumps then loads gives back equivalent Python objects.
count round-trips as an int, so count + 1 is arithmetic; on round-trips as a bool.
Recap
loads= string in, Python objects out (a dict for{}, a list for[]).dumps= Python objects in, a JSON string out.- Types shift at the border:
True/False/None↔true/false/null; numbers stay numbers; keys are always strings. - Two failure modes:
JSONDecodeErrorfor malformed text,KeyErrorfor a missing key — guard withtry/exceptand.get().
Next you will meet larger, nested structures and loop over the data you just learned to parse.