Python's built-in json module makes working with JSON simple and intuitive. Whether you are consuming a REST API, storing configuration, or processing data files, Python's JSON tools handle the job with minimal code. This guide covers every common JSON operation in Python - from basic parsing to custom serialisation, error handling, and working with APIs.
The json Module: Four Core Functions
The json module has four primary functions:
json.loads(string)- parse a JSON string into a Python objectjson.dumps(obj)- serialise a Python object to a JSON stringjson.load(file)- parse JSON from a file objectjson.dump(obj, file)- serialise Python object and write to a file
The "s" in loads and dumps stands for "string" - a helpful way to remember the distinction.
Parsing JSON Strings
import json
# Parse a simple JSON string
json_string = '{"name": "Alice", "age": 30, "active": true}'
data = json.loads(json_string)
print(data["name"]) # Alice
print(data["age"]) # 30 (integer, not string!)
print(data["active"]) # True (Python bool)
print(type(data)) # <class 'dict'>
# Parse an array
array_string = '[1, 2, 3, "four", null, true]'
items = json.loads(array_string)
print(items[3]) # four
print(items[4]) # None (Python's null equivalent)
print(items[5]) # True
JSON types map to Python types as follows:
| JSON Type | Python Type | Example |
|---|---|---|
| object | dict | {"a": 1} → {'a': 1} |
| array | list | [1,2] → [1, 2] |
| string | str | "hello" → 'hello' |
| number (int) | int | 42 → 42 |
| number (float) | float | 3.14 → 3.14 |
| true/false | True/False | true → True |
| null | None | null → None |
Reading and Writing JSON Files
import json
# Read JSON from a file
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
print(config["database"]["host"])
# Write Python dict to a JSON file
data = {
"version": "1.0",
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp"
},
"features": ["auth", "logging", "cache"]
}
with open("config.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# The file will contain formatted JSON:
# {
# "version": "1.0",
# "database": { ... },
# ...
# }
Pretty Printing JSON
import json
data = {"name": "Alice", "scores": [95, 87, 92], "address": {"city": "London"}}
# Compact (default)
print(json.dumps(data))
# {"name": "Alice", "scores": [95, 87, 92], "address": {"city": "London"}}
# Indented - 2 or 4 spaces (your preference)
print(json.dumps(data, indent=2))
# {
# "name": "Alice",
# "scores": [95, 87, 92],
# "address": {
# "city": "London"
# }
# }
# Sorted keys - useful for reproducible output and diffing
print(json.dumps(data, indent=2, sort_keys=True))
# Print directly from a file to inspect it
with open("data.json") as f:
print(json.dumps(json.load(f), indent=2))
Serialising Python Objects
Not all Python objects can be serialised to JSON by default. The following types cause a TypeError:
import json
from datetime import datetime, date
from decimal import Decimal
import uuid
data = {
"created_at": datetime.now(), # TypeError!
"price": Decimal("9.99"), # TypeError!
"id": uuid.uuid4() # TypeError!
}
To handle these types, provide a custom default function or a subclass of json.JSONEncoder:
import json
from datetime import datetime, date
from decimal import Decimal
import uuid
def default_serialiser(obj):
if isinstance(obj, (datetime, date)):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, uuid.UUID):
return str(obj)
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
data = {
"created_at": datetime.now(),
"price": Decimal("9.99"),
"id": uuid.uuid4()
}
# Pass the custom function as the 'default' argument
json_string = json.dumps(data, default=default_serialiser, indent=2)
print(json_string)
Using JSON with the Requests Library
The requests library makes consuming JSON APIs simple:
import requests
# GET request - parse JSON response
response = requests.get("https://api.example.com/users/1")
response.raise_for_status() # raises HTTPError for 4xx/5xx status codes
user = response.json() # automatically parses JSON response
print(user["name"])
# POST request with JSON body
new_user = {
"name": "Bob Smith",
"email": "bob@example.com",
"role": "editor"
}
response = requests.post(
"https://api.example.com/users",
json=new_user, # sets Content-Type: application/json and serialises automatically
headers={"Authorization": "Bearer your-token"}
)
response.raise_for_status()
created_user = response.json()
print(f"Created user with ID: {created_user['id']}")
# PUT request to update a resource
updates = {"name": "Robert Smith", "active": True}
response = requests.put(
f"https://api.example.com/users/{created_user['id']}",
json=updates,
headers={"Authorization": "Bearer your-token"}
)
response.raise_for_status()
Error Handling
import json
# Handle malformed JSON
def safe_parse(json_string):
try:
return json.loads(json_string)
except json.JSONDecodeError as e:
print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
return None
# Handle missing keys safely
data = json.loads('{"user": {"name": "Alice"}}')
# Bad - raises KeyError if email is missing
# email = data["user"]["email"]
# Good - .get() returns None (or a default) for missing keys
email = data.get("user", {}).get("email", "no email provided")
print(email) # no email provided
Processing Large JSON Files
For very large JSON files (gigabytes), loading everything into memory at once is impractical. Use the ijson library for streaming JSON parsing:
import ijson
# Stream through a large array of objects
with open("large_data.json", "rb") as f:
for item in ijson.items(f, "item"):
# process one item at a time without loading the whole file
print(item["name"])
# Stream nested objects
with open("large_data.json", "rb") as f:
for key, value in ijson.kvitems(f, "users.item"):
print(key, value)
Converting JSON to Python Dataclasses
For typed, structured access to JSON data, Python dataclasses (Python 3.7+) combined with the dacite or marshmallow library provide an excellent pattern:
from dataclasses import dataclass
from typing import Optional, List
import json
import dacite
@dataclass
class Address:
street: str
city: str
zip: Optional[str] = None
@dataclass
class User:
id: int
name: str
email: str
address: Address
tags: List[str]
json_string = '''
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"address": {"street": "123 Main St", "city": "New York"},
"tags": ["admin", "developer"]
}
'''
data = json.loads(json_string)
user = dacite.from_dict(data_class=User, data=data)
print(user.name) # Alice
print(user.address.city) # New York
print(user.tags[0]) # admin
Explore Any JSON Structure Before Processing It
Before writing Python code to process a JSON response, use JSON Keyper to extract all key paths. This gives you a complete map of the data structure so you know exactly which fields to access.
Open JSON Keyper