Skip to content

Working with JSON Data

💡
Before you start

You need Python installed, and to know how to run a .py file. If python3 --version in a terminal prints a version number, you are ready. If it does not — or if you have never opened a terminal — do Getting Started with Python first; it installs Python and runs your first program, and takes about fifteen minutes. Nothing else is needed: no account, no payment, no extra software.

What is JSON?

JSON (JavaScript Object Notation) is the most common data format for exchanging information between systems. APIs return JSON, config files use JSON, and databases store JSON. If you work with any external data, you'll work with JSON.

{
    "name": "Alice",
    "age": 25,
    "is_admin": false,
    "skills": ["Python", "Linux", "Security"],
    "address": {
        "city": "London",
        "country": "UK"
    }
}
💡
JSON ↔ Python mapping

JSON objects become Python dicts, JSON arrays become lists, true/false become True/False, and null becomes None.

Parsing JSON Strings

import json

# JSON string → Python dict
json_string = '{"name": "Alice", "age": 25, "active": true}'
data = json.loads(json_string)

print(data["name"])        # Alice
print(data["age"])         # 25
print(type(data))          # <class 'dict'>

# Python dict → JSON string
user = {"name": "Bob", "age": 30, "active": True}
json_output = json.dumps(user)
print(json_output)         # {"name": "Bob", "age": 30, "active": true}

# Pretty-print JSON
print(json.dumps(user, indent=2))
# {
#   "name": "Bob",
#   "age": 30,
#   "active": true
# }

Reading JSON Files

import json

# Read a JSON file
with open("config.json", "r") as file:
    config = json.load(file)         # Note: load (not loads)

print(config["database"]["host"])
print(config["debug"])

Writing JSON Files

import json

data = {
    "users": [
        {"name": "Alice", "role": "admin"},
        {"name": "Bob", "role": "user"}
    ],
    "version": "1.0",
    "last_updated": "2026-03-04"
}

# Write to file (pretty-printed)
with open("data.json", "w") as file:
    json.dump(data, file, indent=2)    # Note: dump (not dumps)
💡
loads/dumps vs load/dump

loads/dumps work with strings (the "s" stands for string). load/dump work with files.

Handling JSON Errors

import json

# Invalid JSON will raise JSONDecodeError
bad_json = '{"name": "Alice", age: 25}'    # Missing quotes around key

try:
    data = json.loads(bad_json)
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")
    # Invalid JSON: Expecting property name enclosed in double quotes

Working with Nested JSON

import json

response = '''
{
    "status": "success",
    "data": {
        "users": [
            {"id": 1, "name": "Alice", "email": "alice@example.com"},
            {"id": 2, "name": "Bob", "email": "bob@example.com"}
        ],
        "total": 2
    }
}
'''

data = json.loads(response)

# Access nested values
print(data["status"])                      # success
print(data["data"]["total"])               # 2

# Loop through nested arrays
for user in data["data"]["users"]:
    print(f"{user['name']}: {user['email']}")

# Safe access with .get() (avoids KeyError)
phone = data["data"]["users"][0].get("phone", "Not provided")
print(phone)    # Not provided

Practical Examples

Config File Manager

import json
from pathlib import Path

class Config:
    def __init__(self, filename):
        self.filename = filename
        self.data = {}
        self._load()

    def _load(self):
        path = Path(self.filename)
        if path.exists():
            with open(self.filename, "r") as f:
                self.data = json.load(f)

    def save(self):
        with open(self.filename, "w") as f:
            json.dump(self.data, f, indent=2)

    def get(self, key, default=None):
        return self.data.get(key, default)

    def set(self, key, value):
        self.data[key] = value
        self.save()

# Usage
config = Config("app_settings.json")
config.set("theme", "dark")
config.set("language", "en")
config.set("notifications", True)
print(config.get("theme"))         # dark

Log Analyzer

import json

# Parse JSON log entries
log_entries = [
    '{"timestamp": "2026-03-04T10:30:00", "level": "INFO", "message": "Server started"}',
    '{"timestamp": "2026-03-04T10:30:05", "level": "ERROR", "message": "Database timeout"}',
    '{"timestamp": "2026-03-04T10:30:10", "level": "INFO", "message": "Retry successful"}',
    '{"timestamp": "2026-03-04T10:31:00", "level": "ERROR", "message": "Disk space low"}'
]

errors = []
for entry_str in log_entries:
    entry = json.loads(entry_str)
    if entry["level"] == "ERROR":
        errors.append(entry)

print(f"Found {len(errors)} errors:")
for error in errors:
    print(f"  [{error['timestamp']}] {error['message']}")

Data Transformation

import json

# Transform JSON data between formats
api_response = '''
[
    {"first_name": "Alice", "last_name": "Smith", "age": 25},
    {"first_name": "Bob", "last_name": "Jones", "age": 30}
]
'''

users = json.loads(api_response)

# Transform to a different format
transformed = {
    user["first_name"].lower(): {
        "full_name": f"{user['first_name']} {user['last_name']}",
        "age": user["age"]
    }
    for user in users
}

print(json.dumps(transformed, indent=2))
# {
#   "alice": {"full_name": "Alice Smith", "age": 25},
#   "bob": {"full_name": "Bob Jones", "age": 30}
# }

JSON Serialization Tips

import json
from datetime import datetime

# Problem: datetime is not JSON serializable
data = {"timestamp": datetime.now()}
# json.dumps(data)   # TypeError!

# Solution: custom serializer
def json_serial(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serializable")

json_output = json.dumps(data, default=json_serial)
print(json_output)    # {"timestamp": "2026-03-04T10:30:00.000000"}

# Other useful dumps options
json.dumps(data, default=json_serial,
           indent=2,              # Pretty print
           sort_keys=True,        # Alphabetical keys
           ensure_ascii=False)    # Allow unicode characters

Now Do It Yourself: Five Steps

Reference above, real task here: you will create a configuration file, read it, reach into the nested parts, break it on purpose so its error never puzzles you, and write your change back to disk. Every output and every error below is what Python actually printed.

1
Create a real JSON file

Go: open a terminal, move to a folder you can write in with cd ~, and open a text editor.

Do: save these six lines as settings.json in that folder.

{
  "site": "finkatana.com",
  "owner": {"name": "Alice", "email": "alice@example.com"},
  "features": ["search", "newsletter"],
  "maxUploadMb": 8
}

You should see: running cat settings.json (or type settings.json on Windows) prints it straight back. Note the shapes already present: an object, a nested object, an array and a plain number.

If not: if your editor added a .txt ending, rename the file — .json must be the last part of the name. The extension is only a label to humans, but you will confuse yourself in thirty seconds without it.

2
Read it into Python and prove it is real data

Go: in the same folder, start Python with python3.

Do: run these four lines.

import json
d = json.load(open("settings.json"))
print(type(d))
print(d["site"], d["maxUploadMb"] + 2)

You should see: <class 'dict'> then finkatana.com 10. That addition is the proof that matters: 8 arrived as a real number, not the text "8", so JSON types survive the trip into Python.

If not: FileNotFoundError means the terminal is in a different folder from the file. Check with pwd, or pass the full path. Do not move the file — move the terminal, with cd.

3
Reach into the nested parts

Go: stay at the >>> prompt.

Do: run these three lines.

print(d["owner"]["email"])
print(d["features"][0])
print(len(d["features"]))

You should see: alice@example.com, search, 2. Nested objects are just dicts inside dicts, so you index them one key at a time; arrays are lists, so they use numeric positions starting at 0.

If not: KeyError: 'theme' — or any other key name — means that key simply is not in the file, and Python refuses to guess. Use d.get("theme") to get None instead of an error, or d.get("theme", "dark") to supply a fallback. That single habit prevents most config-reading crashes.

4
Break it on purpose — the error you will meet most often

Go: leave the prompt with exit() and create a second file, broken.json.

Do: save exactly this — note the comma after the last value — then run python3 -c "import json; json.load(open('broken.json'))".

{
  "site": "finkatana.com",
}

You should see: json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 3 column 1 (char 29). JSON forbids a trailing comma after the final entry, unlike Python. The message gives you the line and column — go there first, every time.

If not: if it loads without complaint, you removed the comma while typing. Put it back and run it again: meeting this error once, deliberately, saves you from a puzzling ten minutes later. The other frequent cause is single quotes — JSON requires double quotes around every key and every string, even though Python accepts either.

5
Change a value and write the file back

Go: create a file called update.py in the same folder.

Do: put these five lines in it and run python3 update.py.

import json
d = json.load(open("settings.json"))
d["features"].append("contact-form")
json.dump(d, open("settings.json", "w"), indent=2)
print(open("settings.json").read())

You should see: the whole file printed back, reformatted onto separate indented lines, with "contact-form" now third in the features array. indent=2 is what makes it readable; without it the file is written as one long line, which is valid but miserable to edit by hand.

If not: if the file comes back empty, you opened it with "w" before reading it — that mode empties the file the instant it opens. Always finish reading before you open for writing. Because this rewrites the original, keep a copy the first few times: cp settings.json settings.json.bak.

🎉
Check yourself before moving on

Without scrolling up: name two things JSON forbids that Python allows in a dictionary literal. If you can, you already avoid the two most common causes of JSONDecodeError. Answer: a trailing comma after the last entry, and single quotes — JSON requires double quotes around every key and string.

Now do it without the page: add a "theme": "dark" key to settings.json from Python and write it back, then read it out again to prove it stuck. Steps 2 and 5, joined up.

Summary

  • json.loads(string) parses a JSON string into Python objects
  • json.dumps(obj) converts Python objects to a JSON string
  • json.load(file) reads JSON from a file
  • json.dump(obj, file) writes JSON to a file
  • Use indent=2 for human-readable output
  • Use .get(key, default) for safe access to nested data
  • Handle json.JSONDecodeError for invalid JSON input
🎉
JSON mastered!

You can now exchange data with APIs, save configurations, and process structured data. Next up: regular expressions — powerful pattern matching for text processing.