Module 6 · Advanced Data Handling ⏱ 16 min

Working with JSON

By the end of this lesson you will be able to:
  • Turn a JSON string into Python objects with json.loads
  • Turn Python objects into a JSON string with json.dumps
  • Read fields out of parsed JSON data

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
json.loads parses a string into objects; json.dumps serializes objects back into a string.

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
Same names, different spelling: Python's True/False/None become JSON's true/false/null and round-trip back.
Round-trip a record and watch the types come back exactly as they went in.
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
Parsing fails two ways: malformed text raises JSONDecodeError; a missing key raises KeyError. Both are catchable.
Defensive parsing: catch bad JSON, and use .get() so a missing key returns None.
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
Exercise

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
Exercise

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

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
Exercise

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
Exercise

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

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/Nonetrue/false/null; numbers stay numbers; keys are always strings.
  • Two failure modes: JSONDecodeError for malformed text, KeyError for a missing key — guard with try/except and .get().

Next you will meet larger, nested structures and loop over the data you just learned to parse.

Checkpoint quiz

Which function converts a JSON string into Python objects?

In valid JSON, how is Python's True written?

Go deeper — technical resources