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 secondTrap 3: The directory doesn’t exist yet.
report_dir.mkdir(parents=True, exist_ok=True) # idempotent — safe to call repeatedlyWhen 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: object → dict, array → list, string → str, number → int/float, true/false → True/False, null → None.
⚠️ 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:
| Function | Input | Use when |
|---|---|---|
json.loads(s) | string | Parsing a JSON string (API response, config string) |
json.load(f) | file object | Parsing a JSON file opened with open() |
json.dumps(obj) | Python object | Converting to a JSON string |
json.dump(obj, f) | Python object + file | Writing 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