Often you only need to change one leaf value inside a JSON file while leaving every sibling intact. Below are the practical patterns that achieve this in pure Python, followed by a lightweight helper that keeps the code footprint minimal yet avoids the "load-modify-dump everything" anti-pattern.
Quick Motivating Example
{
"name": "张三",
"age": 18
}
Goal: bump age to 21 without touching name.
Approaches at a Glance
- Read-then-replace – simplest, but you rewrite the whole file even when only one field changes.
- Named callback – works, yet feels verbose because Python’s
lambdais restricted to a single expression. - Third-party helpers (e.g.
pydash) – ergonomic, but pulling in a libray for a single patch is overkill. - Hand-rolled path walker – what we’ll implement below.
Minimal Path-Based Updater
The idea is to tarverse the structure using a list of keys / indices, mutate the final container in place, then persist the change.
import json
from pathlib import Path
from datetime import datetime
from typing import Any, List, Union
def patch_json(path: Path, pointer: List[Union[str, int]], value: Any) -> bool:
"""
Update a single leaf in a JSON document located at *pointer*.
pointer: list of keys or list indices, e.g. ["users", 0, "profile", "age"]
returns: True if the leaf was found and updated, else False
"""
try:
with path.open(encoding="utf-8") as fp:
doc = json.load(fp)
except (FileNotFoundError, json.JSONDecodeError) as exc:
print(f"Cannot read {path}: {exc}")
return False
cursor = doc
try:
*parents, last = pointer
for step in parents:
cursor = cursor[step] # walk down
cursor[last] = value # mutate in place
except (KeyError, IndexError, TypeError) as exc:
print(f"Invalid pointer {pointer}: {exc}")
return False
try:
with path.open("w", encoding="utf-8") as fp:
json.dump(doc, fp, indent=2, ensure_ascii=False)
except OSError as exc:
print(f"Cannot write {path}: {exc}")
return False
return True
Usage Demo
DB = Path("users.json")
# bootstrap sample file
DB.write_text(json.dumps([
{"hika": {"latest_login": None, "email": "h.nagi@example.com"}, "id": 1},
{"xxgal": {"latest_login": None, "email": "x.gal@example.com"}, "id": 2}
], indent=2), encoding="utf-8")
print("Before:", DB.read_text())
patch_json(DB, [0, "hika", "latest_login"], datetime.now().isoformat(timespec="seconds"))
print("After:", DB.read_text())
Runnning the snippet shows only latest_login changed; every other field, ordering, and formatting remains untouched.
Edge Cases & Tips
- When the leaf does not exist, the helper returns
Falseinstead of creating the path; change the logic if auto-creation is desired. - Lists and dicts are handled transparently; mixed structures such as
{"items": [{"meta": {...}}]}work out of the box. - If atomicity matters, write to a temporary file and
os.replaceit to avoid half-written data on crash.
For most everyday scripts the above 20-line utility is all you need; no external dependency, no full-document rewrite, and the call site stays a one-liner.