PYTHON / FILE HANDLING
Reading and writing JSON files
Save Python dicts and lists to JSON files with json.dump, read them back with json.load, and predict which types survive the round trip.
What you will learn
- Write with json.dump(obj, file) and read with json.load(file); the 's' forms use strings
- Predict the mapping: only dict, list, str, int, float, bool and None are representable
- Update a stored JSON value by loading it, editing in memory, then rewriting the whole file
- Control text output with indent=, ensure_ascii=False and default= for odd objects
Understanding Reading and writing JSON files
A JSON file holds exactly one top-level value, almost always an object or an array. json.dump(obj, f) walks a Python object and writes its JSON text into an already-open text file, and json.load(f) reads the whole file and parses it back into fresh Python objects. The pair without the 's' talks to file objects; json.dumps and json.loads do the same work with a str in memory, which is why passing a filename to json.load fails with AttributeError: 'str' object has no attribute 'read'.
The useful mental model is translation between two type systems, not saving a Python object. dict becomes object, list becomes array, str becomes string, int and float become number, True/False become true/false, and None becomes null. The mapping is not symmetric: a tuple is written as an array and comes back as a list, and a non-string dict key such as 1 is written as "1" and comes back as a string, so a loaded value can differ from what you dumped. Anything with no mapping at all, like a set or a datetime, raises TypeError mid-write, and because json.dump writes as it goes, the file is left half finished.
JSON is defined over Unicode text, so these files are opened in text mode and the encoding matters. Pass encoding="utf-8" on both write and read instead of relying on the platform default, which is still cp1252 on some Windows setups. By default ensure_ascii=True escapes every non-ASCII character as \uXXXX, which is safe anywhere but unreadable; ensure_ascii=False writes the real characters and relies on the file encoding. Because the file holds one value, there is no way to append a record: you load, change the object, and dump it again in full.
import json
data = {
"name": "Ada",
"scores": [91, 78, 100],
"active": True,
"nickname": None,
"position": (3, 4),
}
with open("profile.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
with open("profile.json", encoding="utf-8") as f:
print(f.read())
with open("profile.json", encoding="utf-8") as f:
loaded = json.load(f)
print(type(loaded["position"]).__name__, loaded["position"])
print(loaded == data)json.dump and json.load translate between Python objects and a single JSON text value using a fixed, slightly lossy type mapping.
Worked examples
Adding a record without corrupting the file
Shows why append mode breaks a JSON file and what the load-edit-rewrite cycle looks like instead.
import json
path = "log.json"
with open(path, "w", encoding="utf-8") as f:
json.dump([{"id": 1}], f)
with open(path, "a", encoding="utf-8") as f: # tempting, but wrong
json.dump([{"id": 2}], f)
with open(path, encoding="utf-8") as f:
print(f.read())
f.seek(0)
try:
json.load(f)
except json.JSONDecodeError as e:
print("decode failed:", e.msg, "at char", e.pos)
with open(path, "w", encoding="utf-8") as f:
json.dump([{"id": 1}], f)
with open(path, encoding="utf-8") as f:
records = json.load(f)
records.append({"id": 2})
with open(path, "w", encoding="utf-8") as f:
json.dump(records, f)
with open(path, encoding="utf-8") as f:
print(f.read())Example explained
Line 1Mode "a" places a second complete JSON array right after the first, producing text no JSON parser accepts.
Line 2The parser reads the first array successfully and then reports Extra data at position 11, the index just past that value.
Line 3The fix reads the file into a real Python list, appends to that list, and dumps the whole list again over mode "w".
Line 4Rewriting in full is why the second version produces one array with two objects instead of two arrays.
Values JSON cannot represent
Demonstrates the TypeError for a date, the default= escape hatch, and what ensure_ascii=False changes on disk.
import json
from datetime import date
record = {"city": "Zürich", "visited": date(2026, 9, 1)}
try:
with open("trip.json", "w", encoding="utf-8") as f:
json.dump(record, f)
except TypeError as e:
print("TypeError:", e)
with open("trip.json", "w", encoding="utf-8") as f:
json.dump(record, f, default=str, ensure_ascii=False, sort_keys=True)
with open("trip.json", encoding="utf-8") as f:
print(f.read())
with open("trip.json", encoding="utf-8") as f:
back = json.load(f)
print(type(back["visited"]).__name__, repr(back["visited"]))Example explained
Line 1The first dump raises TypeError only when it reaches the date, so trip.json is left holding a truncated fragment.
Line 2default=str is called for any value the encoder cannot handle, turning the date into the string "2026-09-01".
Line 3ensure_ascii=False writes the literal ü instead of \u00fc; the file is still valid JSON because it is encoded as UTF-8.
Line 4Loading gives back a str, not a date: the conversion is one-way and you must parse it yourself if you need a date.
Important notes
json.dump writes no trailing newline, so a JSON file often has no final line break; add f.write("\n") yourself if a tool requires one.
json.load parses the entire file into memory at once. For a large stream of records, write one compact json.dumps result per line (JSON Lines) and parse a line at a time.
Common mistakes
Opening the file in "a" mode to add an entry: the file ends up with two JSON values and every later json.load raises JSONDecodeError: Extra data.
Calling json.load("data.json") with the path instead of an open file, which raises AttributeError: 'str' object has no attribute 'read' because load expects an object with .read().
Assuming the round trip is lossless: tuples come back as lists, integer dict keys come back as strings, and sets or datetimes raise TypeError partway through writing and leave a corrupt file.
Try it yourself
Change, predict, then run
Write a dict of three book titles mapped to their publication years to books.json with indent=2, then in a separate block load it, add a fourth book, and dump it back. Print the file text and check that only one pair of braces appears.
Open the Python workspaceCheck your understanding
You dump {1: "a", "items": ("x", "y")} to a JSON file and load it back, and the loaded value is not equal to the original. What explains it?
- JSON object keys must be strings and JSON has no tuple type, so 1 became "1" and the tuple became a list
- json.dump sorts the keys by default, so the loaded dict has a different order and compares unequal
- The file was opened without encoding="utf-8", so the values were silently altered on the way to disk
- json.load returns a special mapping type that never compares equal to a plain dict
Show answer
The encoder converts non-string keys to strings and writes tuples as arrays, so you get {"1": "a", "items": ["x", "y"]} back. Sorting is not the cause: sort_keys is False by default, and dict equality ignores key order anyway.