← Home

101. Json Module

JSON serialization and deserialization

101. Json Module

“I want to look at last week’s RAM history.” Time to save to disk.

Rohan wants to persist each snapshot as a JSON file. Three traps he falls into:

Trap 1: str(snapshot) writes Python repr, not valid JSON.

# Wrong — produces {'percent': 80.1} with single quotes, not valid JSON
f.write(str(snapshot))

# Right — produces {"percent": 80.1} with double quotes, valid JSON
json.dump(snapshot, f, indent=2)

Trap 2: Every report overwrites the last because the filename is always report.json.

ts = datetime.now().strftime('%Y%m%d_%H%M%S')
path = report_dir / f'report_{ts}.json'   # unique every second

Trap 3: The directory doesn’t exist yet.

report_dir.mkdir(parents=True, exist_ok=True)  # idempotent — safe to call repeatedly

When reading back a report, he mixes up json.load and json.loads:

# json.load(f)    → reads from a file object (open file)
# json.loads(s)   → reads from a string (API response, config string)

He also learns to use .get() for safe key access — so missing optional fields don’t crash the whole report reader.

JSON was invented in 2001 by Douglas Crockford. He said he “discovered” it rather than invented it — it was already implicit in JavaScript. Now it’s the lingua franca of the entire internet. Every API, every config file, every AWS bill uses it.

💡 Fun fact: The json module was added to Python’s standard library in Python 2.6 (2008). Before that, Python developers used third-party libraries like simplejson. The module uses the RFC 8259 standard. JSON’s type system maps cleanly to Python: objectdict, arraylist, stringstr, numberint/float, true/falseTrue/False, nullNone.

⚠️ Watch out: json.dumps() cannot serialize Python objects like datetime, set, or custom classes by default — it raises TypeError: Object of type datetime is not JSON serializable. You must either convert them to strings first, or pass a custom default= function to json.dumps().

🤔 Think about it: json.loads() accepts a string and json.load() accepts a file object. If you have a file object, could you do json.loads(f.read()) instead of json.load(f)? What would be the downside of always using json.loads(f.read()) for files?

Learning objectives

  • Distinguish between json.load() (file) and json.loads() (string)
  • Use dict.get() for safe key access with defaults
  • Serialize Python objects to JSON strings with json.dumps()
  • Handle json.JSONDecodeError for invalid input

Key concepts

  • json.loads() / json.load() — deserialization
  • json.dumps() / json.dump() — serialization
  • dict.get(key, default) — safe access
  • json.JSONDecodeError — error handling
  • indent= parameter for pretty printing

Try it

Concept detail

JSON in Python

The json module is Python’s built-in JSON parser. Two key function pairs:

FunctionInputUse when
json.loads(s)stringParsing a JSON string (API response, config string)
json.load(f)file objectParsing a JSON file opened with open()
json.dumps(obj)Python objectConverting to a JSON string
json.dump(obj, f)Python object + fileWriting JSON to a file

Safe Key Access

Never use dict[key] when the key might be missing. Use dict.get(key, default):

# Raises KeyError if "theme" not in config:
theme = config["theme"]

# Returns "light" if "theme" not in config:
theme = config.get("theme", "light")

Python dict vs JSON object

Python uses single quotes for strings; JSON requires double quotes. str({"key": "val"}) produces {'key': 'val'} — not valid JSON. json.dumps({"key": "val"}) produces '{"key": "val"}' — valid JSON.

Error handling

try:
    data = json.loads(raw_string)
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")

Solution

import json

def parse_config(config_str: str) -> dict:
    """Parse a JSON config string and return settings dict."""
    data = json.loads(config_str)  # loads() for strings, load() for files
    return data

def get_setting(config: dict, key: str, default=None):
    """Safely get a setting, returning default if key missing."""
    return config.get(key, default)  # .get() never raises KeyError

def build_summary(config: dict) -> str:
    """Build a human-readable config summary."""
    name = get_setting(config, "app_name", "Unknown App")
    version = get_setting(config, "version", "0.0.0")
    debug = get_setting(config, "debug", False)
    return f"{name} v{version} (debug={'ON' if debug else 'OFF'})"

Tests

VALID_JSON = '{"app_name": "PyVault", "version": "1.2.0", "debug": true}'
MINIMAL_JSON = '{"app_name": "MinimalApp"}'

def test_parse_config_returns_dict():
    result = parse_config(VALID_JSON)
    assert isinstance(result, dict), "parse_config should return a dict"

def test_parse_config_reads_values():
    result = parse_config(VALID_JSON)
    assert result["app_name"] == "PyVault"
    assert result["version"] == "1.2.0"
    assert result["debug"] is True

def test_get_setting_existing_key():
    config = parse_config(VALID_JSON)
    assert get_setting(config, "app_name") == "PyVault"

def test_get_setting_missing_key_returns_default():
    config = parse_config(VALID_JSON)
    result = get_setting(config, "nonexistent_key", "fallback")
    assert result == "fallback"

def test_get_setting_missing_key_default_none():
    config = parse_config(VALID_JSON)
    result = get_setting(config, "missing")
    assert result is None

def test_build_summary_full_config():
    config = parse_config(VALID_JSON)
    summary = build_summary(config)
    assert "PyVault" in summary
    assert "1.2.0" in summary
    assert "ON" in summary  # debug=True → "ON"

def test_build_summary_minimal_config():
    config = parse_config(MINIMAL_JSON)
    summary = build_summary(config)
    assert "MinimalApp" in summary
    assert "0.0.0" in summary  # default version
    assert "OFF" in summary    # default debug=False

Resources