JSON Basics
Python's built-in json module converts between Python objects and JSON strings.
import json
# Python -> JSON string: json.dumps()
data = {
"name": "Alice",
"age": 30,
"active": True,
"score": 9.5,
"tags": ["python", "developer"],
"address": None,
}
json_string = json.dumps(data)
print(json_string)
# {"name": "Alice", "age": 30, "active": true, "score": 9.5, ...}
# Pretty printing
pretty = json.dumps(data, indent=2)
print(pretty)
# {
# "name": "Alice",
# "age": 30,
# ...
# }
# Options
compact = json.dumps(data, separators=(",", ":")) # no spaces - smallest output
sorted_ = json.dumps(data, indent=2, sort_keys=True) # alphabetical keys
# JSON string -> Python object: json.loads()
json_str = '{"name": "Bob", "age": 25, "active": false}'
obj = json.loads(json_str)
print(obj["name"]) # Bob
print(type(obj["active"])) #
# JSON <-> Python type mapping
print(json.dumps(None)) # null
print(json.dumps(True)) # true
print(json.dumps(False)) # false
print(json.dumps(42)) # 42
print(json.dumps(3.14)) # 3.14
print(json.dumps("hello")) # "hello"
print(json.dumps([1, 2])) # [1, 2]
print(json.dumps({"a": 1})) # {"a": 1}
| Python | JSON |
|---|---|
dict | object {} |
list, tuple | array [] |
str | string |
int, float | number |
True / False | true / false |
None | null |
Reading and Writing Files
import json
from pathlib import Path
data = {
"users": [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
],
"total": 2,
}
# Write JSON to file
with open("users.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
# ensure_ascii=False preserves non-ASCII characters (emoji, accented chars)
# Read JSON from file
with open("users.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["total"]) # 2
print(loaded["users"][0]["name"]) # Alice
# Shorthand with pathlib
path = Path("config.json")
path.write_text(json.dumps({"debug": True, "port": 8080}, indent=2))
config = json.loads(path.read_text())
# Read JSON with error handling
def load_json_file(filepath):
try:
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return {}
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in {filepath}: {e}") from None
config = load_json_file("settings.json")
# Merge and save back
def update_json_file(filepath, updates):
data = load_json_file(filepath)
data.update(updates)
with open(filepath, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
Custom Encoder/Decoder
Handle Python types that JSON cannot represent natively (datetime, Decimal, custom classes).
import json
import datetime
from decimal import Decimal
# Method 1: default= parameter with a function
def json_default(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat() # "2024-06-15T14:30:00"
if isinstance(obj, datetime.date):
return obj.isoformat() # "2024-06-15"
if isinstance(obj, Decimal):
return float(obj) # or str(obj) for precision
if isinstance(obj, set):
return sorted(list(obj)) # set -> sorted list
raise TypeError(f"Not serializable: {type(obj)}")
data = {
"created": datetime.datetime.now(),
"price": Decimal("19.99"),
"tags": {"python", "coding"},
}
print(json.dumps(data, default=json_default, indent=2))
# Method 2: Custom JSONEncoder subclass
class AppEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime, datetime.date)):
return obj.isoformat()
if isinstance(obj, Decimal):
return str(obj) # keep precision as string
return super().default(obj) # raises TypeError for unknown types
print(json.dumps(data, cls=AppEncoder, indent=2))
# Custom decoder with object_hook
def datetime_decoder(dct):
for key, value in dct.items():
if isinstance(value, str) and "T" in value:
try:
dct[key] = datetime.datetime.fromisoformat(value)
except ValueError:
pass
return dct
json_str = '{"name": "Event", "start": "2024-06-15T10:00:00"}'
obj = json.loads(json_str, object_hook=datetime_decoder)
print(type(obj["start"])) #
REST APIs with requests
The requests library (third-party, widely used) makes HTTP calls simple. Install with pip install requests.
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
# GET request
response = requests.get(f"{BASE_URL}/posts/1")
print(response.status_code) # 200
print(response.ok) # True (status 200-299)
# Parse JSON response
post = response.json()
print(post["title"])
# GET with query parameters
params = {"userId": 1, "_limit": 3}
response = requests.get(f"{BASE_URL}/posts", params=params)
posts = response.json()
print(f"Got {len(posts)} posts")
# POST request with JSON body
new_post = {"title": "Hello", "body": "World", "userId": 1}
response = requests.post(
f"{BASE_URL}/posts",
json=new_post, # automatically sets Content-Type: application/json
)
created = response.json()
print(created["id"]) # 101
# PUT / PATCH / DELETE
response = requests.put(
f"{BASE_URL}/posts/1",
json={"id": 1, "title": "Updated", "body": "Content", "userId": 1},
)
response = requests.patch(f"{BASE_URL}/posts/1", json={"title": "Patched"})
response = requests.delete(f"{BASE_URL}/posts/1")
print(response.status_code) # 200
# Custom headers (authentication, content type)
headers = {
"Authorization": "Bearer your-token-here",
"Accept": "application/json",
}
response = requests.get(f"{BASE_URL}/posts", headers=headers)
# Timeout - always set one
response = requests.get(f"{BASE_URL}/posts/1", timeout=5) # 5 seconds
# Session - reuse connection and share headers
with requests.Session() as session:
session.headers.update({"Authorization": "Bearer token"})
r1 = session.get(f"{BASE_URL}/posts/1")
r2 = session.get(f"{BASE_URL}/posts/2")
# Error handling
def api_get(url, **kwargs):
try:
response = requests.get(url, timeout=10, **kwargs)
response.raise_for_status() # raises HTTPError for 4xx/5xx
return response.json()
except requests.exceptions.ConnectionError:
raise RuntimeError("Network unavailable")
except requests.exceptions.Timeout:
raise RuntimeError("Request timed out")
except requests.exceptions.HTTPError as e:
raise RuntimeError(f"API error {e.response.status_code}: {e}")
Error Handling
import json
# json.JSONDecodeError when parsing invalid JSON
invalid_jsons = [
"{name: 'Alice'}", # keys must be quoted
"{'name': 'Alice'}", # single quotes not valid in JSON
"{\"name\": \"Alice\",}", # trailing comma
"undefined", # not JSON
]
for s in invalid_jsons:
try:
json.loads(s)
except json.JSONDecodeError as e:
print(f"Error: {e.msg} at line {e.lineno} col {e.colno}")
# TypeError when serializing unsupported types
unsupported = {"data": {1, 2, 3}} # set is not JSON serializable
try:
json.dumps(unsupported)
except TypeError as e:
print(f"Cannot serialize: {e}")
# Safe parsing utility
def safe_parse(text, default=None):
try:
return json.loads(text)
except (json.JSONDecodeError, TypeError):
return default
result = safe_parse('{"valid": true}') # {'valid': True}
result = safe_parse("bad json", {}) # {}
# Validate structure after parsing
def parse_user_api_response(text):
data = json.loads(text) # may raise JSONDecodeError
if not isinstance(data, dict):
raise ValueError("Expected JSON object")
if "name" not in data:
raise ValueError("Missing required field: name")
if not isinstance(data.get("age"), int):
raise ValueError("age must be an integer")
return data
By default, json.dumps() escapes all non-ASCII characters ("cafe" -> "caf\u00e9"). Pass ensure_ascii=False to keep Unicode characters as-is: json.dumps(data, ensure_ascii=False). This produces smaller, more readable output when working with non-English content. Always pair it with encoding="utf-8" when writing to file.