Python JSON

Work with JSON data in Python: serializing and parsing, reading and writing files, custom encoders, and REST API calls with requests.

Beginner 10 min read 9 examples

JSON Basics

Python's built-in json module converts between Python objects and JSON strings.

Python
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}
PythonJSON
dictobject {}
list, tuplearray []
strstring
int, floatnumber
True / Falsetrue / false
Nonenull

Reading and Writing Files

Python
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).

Python
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.

Python
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

Python
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
Use ensure_ascii=False for international text

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.

Frequently Asked Questions

JSON supports: strings, numbers (no distinction between int and float), booleans, null, objects, arrays. Python supports many more types. Mismatches: Python None becomes JSON null; Python True/False become JSON true/false. Python types that JSON cannot represent natively: datetime, set, bytes, Decimal, custom objects. For these, you need a custom encoder.

json.dumps(obj) serializes to a string. json.dump(obj, file) serializes directly to a file object. Similarly, json.loads(string) parses from a string, while json.load(file) parses from a file object. The rule: s at the end = string variant.

Install the requests library (pip install requests) and use response = requests.get(url). Check response.ok or response.status_code, then parse JSON with response.json(). For the standard library (no install needed), use urllib.request.urlopen(url) - but requests is far more ergonomic and is the industry standard.

Use json.dumps(data, indent=2) to get a formatted multi-line string. The indent parameter specifies the number of spaces per indent level. Add sort_keys=True for alphabetically sorted keys. To print directly: print(json.dumps(data, indent=2)).