JSON (JavaScript Object Notation) is the standard format for exchanging data between systems — between a frontend and a backend, between microservices, between your Python script and an external API. Python's built-in json module makes working with JSON straightforward: converting Python objects to JSON strings, parsing JSON strings back into Python objects, and reading and writing JSON files.
The json Module
The json module is part of Python's standard library — no installation required:
import json
It provides four core functions:
| Function | Direction | What It Does |
|---|---|---|
json.dumps() | Python → JSON string | Serialises a Python object to a JSON-formatted string |
json.loads() | JSON string → Python | Parses a JSON string into a Python object |
json.dump() | Python → JSON file | Writes a Python object as JSON to a file |
json.load() | JSON file → Python | Reads JSON from a file into a Python object |
The naming follows a consistent pattern: dumps/loads work with strings (s = string); dump/load work with files.
Serialisation — json.dumps()
Serialisation converts a Python object into a JSON string. This is what you do before sending data over a network or storing it as text:
import json
user = {
"name": "Wariz",
"age": 20,
"city": "Lagos",
"is_active": True,
"score": None,
"tags": ["developer", "founder"]
}
json_string = json.dumps(user)
print(json_string)
# {"name": "Wariz", "age": 20, "city": "Lagos", "is_active": true, "score": null, "tags": ["developer", "founder"]}
Pretty printing with indent
By default, json.dumps() produces a compact single-line string. Use indent for human-readable output:
print(json.dumps(user, indent=4))
{
"name": "Wariz",
"age": 20,
"city": "Lagos",
"is_active": true,
"score": null,
"tags": [
"developer",
"founder"
]
}
Sorting keys — sort_keys
json.dumps(user, indent=4, sort_keys=True)
# Keys appear in alphabetical order
Custom separators
For the most compact output (no spaces):
json.dumps(user, separators=(",", ":"))
# {"name":"Wariz","age":20,...}
Deserialisation — json.loads()
Deserialisation parses a JSON string back into a Python object:
json_string = '{"name": "Wariz", "age": 20, "is_active": true}'
data = json.loads(json_string)
print(data["name"]) # "Wariz"
print(data["age"]) # 20
print(type(data["age"])) # <class 'int'>
Type mapping — JSON to Python
When deserialising, JSON types are converted to their Python equivalents:
| JSON Type | Python Type |
|---|---|
string | str |
number (integer) | int |
number (decimal) | float |
true / false | True / False |
null | None |
object | dict |
array | list |
Type mapping — Python to JSON
The reverse conversion when serialising:
| Python Type | JSON Type |
|---|---|
str | string |
int | number |
float | number |
True / False | true / false |
None | null |
dict | object |
list, tuple | array |
Working with JSON Files
Reading JSON from a file — json.load()
import json
with open("data.json", "r") as file:
data = json.load(file)
print(data["name"]) # Access like a regular Python dict
Writing JSON to a file — json.dump()
import json
user = {"name": "Wariz", "age": 20, "city": "Lagos"}
with open("data.json", "w") as file:
json.dump(user, file, indent=4)
The file now contains:
{
"name": "Wariz",
"age": 20,
"city": "Lagos"
}
Handling JSON from APIs
The most common real-world use of JSON in Python is working with API responses. Here is a typical pattern using the requests library:
import requests
response = requests.get("https://jsonplaceholder.typicode.com/users/1")
# The response body is a JSON string
# requests provides a convenience method to parse it
user = response.json()
print(user["name"]) # "Leanne Graham"
print(user["email"]) # "Sincere@april.biz"
response.json() internally calls json.loads(response.text) — it is a shortcut, not a different function.
With aiohttp (async)
import aiohttp
import asyncio
async def fetch_user(user_id):
async with aiohttp.ClientSession() as session:
async with session.get(f"https://jsonplaceholder.typicode.com/users/{user_id}") as response:
return await response.json() # Parses JSON asynchronously
user = asyncio.run(fetch_user(1))
print(user["name"])
Nested JSON
Real-world API responses are often deeply nested. Access nested data by chaining keys:
data = {
"user": {
"name": "Wariz",
"address": {
"city": "Lagos",
"country": "Nigeria"
},
"skills": ["Python", "JavaScript", "React"]
}
}
print(data["user"]["address"]["city"]) # "Lagos"
print(data["user"]["skills"][0]) # "Python"
For deeply nested structures, use .get() to avoid KeyError on missing keys:
city = data.get("user", {}).get("address", {}).get("city", "Unknown")
Handling JSON Errors
Invalid JSON — json.JSONDecodeError
If the string is not valid JSON, json.loads() raises json.JSONDecodeError:
import json
try:
data = json.loads("this is not json")
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
Non-serialisable types
Not all Python objects can be converted to JSON. Attempting to serialise unsupported types raises TypeError:
import json
from datetime import datetime
data = {"created_at": datetime.now()}
json.dumps(data) # ❌ TypeError: Object of type datetime is not JSON serializable
Handling non-serialisable types with a custom encoder
Pass a default function to handle types that json cannot serialise automatically:
import json
from datetime import datetime
def custom_encoder(obj):
if isinstance(obj, datetime):
return obj.isoformat() # Convert datetime to ISO 8601 string
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
data = {"name": "Wariz", "created_at": datetime.now()}
print(json.dumps(data, default=custom_encoder, indent=4))
Alternatively, subclass json.JSONEncoder:
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
json.dumps(data, cls=CustomEncoder, indent=4)
Validating JSON Structure
Python's json module only checks that a string is syntactically valid JSON — it does not validate that the structure matches an expected schema. For schema validation, use the jsonschema library:
pip install jsonschema
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name", "age"]
}
try:
validate(instance={"name": "Wariz", "age": 20}, schema=schema)
print("Valid")
except ValidationError as e:
print(f"Invalid: {e.message}")
Summary
| Function | Use |
|---|---|
json.dumps(obj) | Python object → JSON string |
json.dumps(obj, indent=4) | Pretty-printed JSON string |
json.dumps(obj, sort_keys=True) | JSON string with sorted keys |
json.loads(string) | JSON string → Python object |
json.dump(obj, file) | Write Python object as JSON to a file |
json.load(file) | Read JSON from a file into a Python object |
response.json() | Parse JSON from an HTTP response (requests) |
json.JSONDecodeError | Exception raised for invalid JSON |
default=func | Custom handler for non-serialisable types |