Python Module 8 – Files, paths and data formats
JSON: load, dump and custom types
Convert between JSON text and Python objects with json.loads, dumps, load and dump, format output for diffs, store datetime and Decimal, and validate data.
What you will learn
- Convert between JSON text and Python objects, and predict what a round trip changes
- Read and write JSON files, and report a syntax error with its line
- Format JSON with indentation and sorted keys for readable diffs
- Serialise custom types with default= and restore them with object_hook
- Validate the shape, numbers and ranges of JSON you load
Before you start
What you’ll learn · Before you start
What you will learn
- Convert between JSON text and Python objects, and predict what a round trip changes
- Read and write JSON files, and report a syntax error with its line
- Format JSON with indentation and sorted keys for readable diffs
- Serialise custom types with default= and restore them with object_hook
- Validate the shape, numbers and ranges of JSON you load
Before you start
On this page
json.loads(json.dumps(order)) gives back a dict that is not equal to the order it started from: a tuple comes back
as a list and the key 3 as the string "3". JSON has fewer types than Python, so a
round trip: Turning data into another form and back, such as a dict into JSON text and back again. A round trip that loses nothing gives an equal value; through JSON, tuples come back as lists and keys as strings. can change your data, and an identifier above 2⁵³ can come back as another
number in any program that reads JSON numbers as floats.
JSON is a text format for data: objects in braces, arrays in brackets, strings in double quotes, numbers, true,
false and null. Python’s json module converts between that text and ordinary Python objects.
json.loads(text) turns JSON text into dictionaries, lists, strings, numbers, booleans and None, and
json.dumps(obj) turns such objects back into text. Their partners json.load(f) and json.dump(obj, f) do the
same with an open file. The s at the end of loads and dumps stands for string.
loads() and dumps() turn JSON text into objects and back
Each JSON type has one Python type it turns into, and each Python type that json can write has one JSON form:
| JSON | Python, after loads() |
Python types that dumps() writes this way |
|---|---|---|
object {...} |
dict |
dict |
array [...] |
list |
list, tuple |
string "..." |
str |
str |
| number, whole | int |
int |
| number, with a fraction or exponent | float |
float |
true, false |
True, False |
True, False |
null |
None |
None |
The table already shows where a round trip loses information. To serialise: To write data in a format that can be stored or sent, such as a dict as JSON text with json.dumps(); reading it back is deserialising. Only the types the format knows come back without help. data is to write it in a format such as JSON, and only what the format can say survives. Step through an order going to JSON and back:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The order. The order holds an int, a tuple of two items, True, None, a float, and one key that is the int 3 rather than a string.
- JSON forms. dumps() writes each value in its JSON form. True becomes true, None becomes null, and the tuple becomes an array, the only kind of list JSON has.
- Keys are strings. The keys of a JSON object are always strings, so the key 3 is written as "3".
- Just text. The result is a str, text that can go into a file, a request or a message, and nothing in it says which Python types it came from.
- Back to objects. loads() turns every array into a list and keeps every key as the string it finds.
- Not the same dict. back == order is False. The tuple came back as a list and the key 3 as "3", so code that reads JSON should expect lists and string keys.
import json
order = {"id": 17, "items": ("pen", "notebook"), "paid": True, "coupon": None, "total": 49.5, 3: "third shelf"}
text = json.dumps(order) # Python object -> JSON text (a str)
print(text)
print(type(text).__name__)
back = json.loads(text) # JSON text -> Python objects
print(back)
print("same as before?", back == order)
print("items is now a", type(back["items"]).__name__, "| keys:", list(back)) Output
{"id": 17, "items": ["pen", "notebook"], "paid": true, "coupon": null, "total": 49.5, "3": "third shelf"}
str
{'id': 17, 'items': ['pen', 'notebook'], 'paid': True, 'coupon': None, 'total': 49.5, '3': 'third shelf'}
same as before? False
items is now a list | keys: ['id', 'items', 'paid', 'coupon', 'total', '3']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 round_trip.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
dumps() returned a str, which you can send over a network or write to a file. The order that came back is not
equal to the one that went out: the tuple became a list, because JSON has only arrays, and the key 3 became the
string "3", because the keys of a JSON object are always strings. Code that reads JSON should expect lists and
string keys, whatever the code that wrote it used. Press Edit, make items a list and write the key 3 as the
string "3", then run it again: this time the round trip gives back an equal dict.
Checkpoint
Check: JSON types and Python types
load() and dump() read and write JSON files
json.load() reads JSON from a file object and json.dump() writes to one. A settings file is the classic use. Step
through the life of one, from the first run to a broken hand edit:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- First run. load_settings() tries to open settings.json. There is no file yet, so the FileNotFoundError is caught and the app starts from its defaults.
- A change. The user picks the dark theme, and marks.csv joins the list of recent files.
- Save. save_settings() writes the dict with json.dump(…, indent=2), one setting per line, so that a person can read and edit the file.
- Next run. load_settings() reads the file with json.load() and merges it over the defaults with |, so a setting added in a later version still gets its default.
- A hand edit. Someone edits the file by hand and leaves a comma after the last setting, which JSON does not allow.
- The error says where. json.load() raises JSONDecodeError. Its msg, lineno and colno give "Illegal trailing comma before end of object at line 3, column 18", which is what the user needs to see.
Here is the program the figure follows:
import json
def defaults():
"""A new dict on every call, so that changing the settings never changes the defaults."""
return {"theme": "light", "font_size": 14, "recent": []}
def load_settings(path):
"""The saved settings, with defaults for anything missing; the defaults alone if there is no file yet."""
try:
with open(path, encoding="utf-8") as f:
saved = json.load(f) # load: from a file object
except FileNotFoundError:
return defaults()
return defaults() | saved
def save_settings(path, settings):
with open(path, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2) # dump: to a file object
settings = load_settings("settings.json")
print("first run:", settings)
settings["theme"] = "dark"
settings["recent"].append("marks.csv")
save_settings("settings.json", settings)
with open("settings.json", encoding="utf-8") as f:
print(f.read())
print("next run:", load_settings("settings.json"))
# A file someone edited by hand, with a trailing comma, which JSON does not allow.
with open("settings.json", "w", encoding="utf-8") as f:
f.write('{\n "theme": "dark",\n "font_size": 16,\n}\n')
try:
load_settings("settings.json")
except json.JSONDecodeError as err:
print(f"{err.msg} at line {err.lineno}, column {err.colno}") Output
first run: {'theme': 'light', 'font_size': 14, 'recent': []}
{
"theme": "dark",
"font_size": 14,
"recent": [
"marks.csv"
]
}
next run: {'theme': 'dark', 'font_size': 14, 'recent': ['marks.csv']}
Illegal trailing comma before end of object at line 3, column 18
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 settings_file.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The program starts with defaults when no file exists, merges the saved settings over the defaults with | so that a
setting added in a later version still gets a value, and writes the file with indent=2 so that a person can read
and edit it. Open JSON files with encoding="utf-8": RFC 8259, the JSON standard, requires UTF-8 for JSON that
systems exchange.
Hand-edited files break in predictable ways, and the last lines show one. JSON allows no trailing comma, no comments,
no single quotes and no unquoted keys. json.JSONDecodeError, a subclass of ValueError, carries the problem in
msg and its place in lineno and colno, which is what an error message for the user should report.
Mixing up load() and loads()
json.load(text) fails because load() expects a file object, and json.loads(f) fails because loads() expects
a string. The functions with an s take and return strings; the others read from and write to files.
Checkpoint
Check: JSON files and their errors
Indenting and sorting keys make files people can read
By default dumps() writes everything on one line and escapes every character outside ASCII. Three options change
that, and a fourth makes the output as small as it can be:
import json
students = [{"name": "अनन्या", "city": "Nagpur", "marks": [91, 88]}, {"name": "Zoë", "city": "Pune", "marks": [76, 95]}]
print(json.dumps(students[0])) # ensure_ascii=True (the default) escapes every non-ASCII character
print(json.dumps(students[0], ensure_ascii=False)) # keeps the characters themselves
# indent and sort_keys give stable, readable files that diff well in version control.
print(json.dumps(students[1], indent=2, sort_keys=True, ensure_ascii=False))
compact = json.dumps(students, separators=(",", ":"), ensure_ascii=False)
spaced = json.dumps(students, ensure_ascii=False)
print(len(compact.encode("utf-8")), "bytes compact,", len(spaced.encode("utf-8")), "bytes with the default spaces") Output
{"name": "\u0905\u0928\u0928\u094d\u092f\u093e", "city": "Nagpur", "marks": [91, 88]}
{"name": "अनन्या", "city": "Nagpur", "marks": [91, 88]}
{
"city": "Pune",
"marks": [
76,
95
],
"name": "Zoë"
}
109 bytes compact, 122 bytes with the default spaces
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 readable.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The table sums up what each option did to the two students:
| Option | What it changes | In the example |
|---|---|---|
ensure_ascii=False |
keeps characters such as अनन्या and Zoë as they are | the name instead of six \u escapes |
indent=2 |
puts each item on its own line, indented by nesting depth | Zoë’s record over eight lines |
sort_keys=True |
writes the keys of every object in sorted order | city, marks, name |
separators=(",", ":") |
drops the spaces after commas and colons | 109 bytes instead of 122 |
Escaped or not, the two forms are valid JSON and load to the same string, but only one can be read; write such text
to a file opened with encoding="utf-8". Sorted keys make the same data always give the same text, so version
control shows only the values that really changed, and tests can compare output with expected text. The compact
separators made the output about 11 % smaller here, which suits data sent over a network more than files that people
read.
Leaving ensure_ascii on for files that people read
json.dump(data, f) writes every character outside ASCII as an escape, so अनन्या becomes six of them. Pass
ensure_ascii=False, and open the file with encoding="utf-8".
Pretty-printing from the command line
python -m json file.json prints a file formatted with an indent of four spaces, --sort-keys and --indent change
that, and invalid JSON gives an error with its line and column:
import subprocess
import sys
with open("order.json", "w", encoding="utf-8") as f:
f.write('{"total": 49.5, "id": 17, "items": ["pen", "notebook"]}')
# python -m json (Python 3.14) pretty-prints a file; --sort-keys and --indent are two of its options.
result = subprocess.run([sys.executable, "-m", "json", "--sort-keys", "--indent", "2", "order.json"],
capture_output=True, text=True)
print(result.stdout, end="")
with open("broken.json", "w", encoding="utf-8") as f:
f.write('{"id": 17, "items": ["pen" "notebook"]}')
result = subprocess.run([sys.executable, "-m", "json", "broken.json"], capture_output=True, text=True)
print("exit status", result.returncode, "|", result.stderr, end="") Output
{
"id": 17,
"items": [
"pen",
"notebook"
],
"total": 49.5
}
exit status 1 | Expecting ',' delimiter: line 1 column 28 (char 27)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cli.py
In a terminal, Python 3.14 also colours the output; the colours disappear when the output goes to a file or another
program, as it does here. On older versions, run python -m json.tool with the same options.
Checkpoint
Check: Output that people can read
A default function writes the types JSON lacks
dumps() refuses any object it has no rule for, and Python 3.14 adds a note that says where in the data the value
was. You supply the rule with default=, a function that dumps() calls for every value it cannot write. Step
through the four values of an order, first without the rule and then with it:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- id. id is 501, an int, a type JSON has, so dumps() writes it as it is.
- Stopped at amount. amount is a Decimal. Without default=, dumps() stops here with TypeError, and Python 3.14 adds a note that names the key, amount.
- encode() for the Decimal. With default=encode, dumps() calls encode() for the Decimal instead, and it returns the string "1999.90", so no digit is rounded away.
- A set. tags is a set, which JSON lacks too. encode() returns a sorted list, so the text is the same on every run.
- A datetime. created_at is a datetime. encode() returns its isoformat() text, time zone included.
- The text. Every value now has a JSON form, and dumps() returns the text with amount and created_at as strings and tags as a list.
- The hook. loads() calls object_hook with every object it decodes. decode() turns the strings under keys that end in _at back into datetime objects.
- What came back. amount and tags come back as a string and a list. The text kept no record of their types, so only a rule such as _at, or a schema both sides share, can restore them.
import json
from datetime import datetime, timezone
from decimal import Decimal
order = {"id": 501, "amount": Decimal("1999.90"), "tags": {"gift", "express"},
"created_at": datetime(2025, 3, 14, 9, 30, tzinfo=timezone.utc)}
try:
json.dumps(order)
except TypeError as err:
print("TypeError:", err)
print("notes:", err.__notes__) # Python 3.14 says where the value was
def encode(value):
"""Called by dumps() for every value it cannot write itself."""
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, Decimal):
return str(value) # a string keeps every digit
if isinstance(value, set):
return sorted(value) # a list, in a fixed order
raise TypeError(f"cannot store {type(value).__name__} as JSON")
text = json.dumps(order, default=encode)
print(text)
def decode(obj):
"""Called by loads() for every JSON object: turn the *_at fields back into datetimes."""
for key, value in obj.items():
if key.endswith("_at"):
obj[key] = datetime.fromisoformat(value)
return obj
back = json.loads(text, object_hook=decode)
print(repr(back["created_at"]))
print(repr(back["amount"]), "and", back["tags"], "(JSON kept no record that they were a Decimal and a set)") Output
TypeError: Object of type Decimal is not JSON serializable
notes: ["when serializing dict item 'amount'"]
{"id": 501, "amount": "1999.90", "tags": ["express", "gift"], "created_at": "2025-03-14T09:30:00+00:00"}
datetime.datetime(2025, 3, 14, 9, 30, tzinfo=datetime.timezone.utc)
'1999.90' and ['express', 'gift'] (JSON kept no record that they were a Decimal and a set)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 custom_types.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
encode() returns something JSON can store for each type it knows and raises TypeError for the rest, as dumps()
itself would. Each choice keeps information: isoformat() writes a datetime as
ISO 8601: The international standard for writing dates and times year first, then month, day and time, with an offset such as +05:30 or Z for UTC. isoformat() writes it and fromisoformat() reads it. More in “Dates, times and time zones (IST included)” text with its time zone, a Decimal becomes a string so that no digit is
rounded away, and a set becomes a sorted list so that the output is the same on every run:
| Value of the order | What encode() returns |
After loads() with the hook |
|---|---|---|
amount, a Decimal |
the string "1999.90" |
still a string |
tags, a set |
the sorted list ["express", "gift"] |
a list |
created_at, a datetime |
its ISO 8601 text | a datetime again |
For the way back, object_hook is called with every JSON object that loads() decodes, and its return value takes
the object’s place; here it turns the strings under keys ending in _at back into datetime objects. The last line
shows the limit of the approach. The JSON text says nothing about types, so the reader can restore a value only when
it knows what to expect, through a naming rule such as _at, a field that names the type, or a schema that both
sides share. When many types are involved, subclass json.JSONEncoder and override its default() method instead of
passing a function each time.
Version note
New in Python 3.14: errors raised while serialising carry notes that name the dictionary key or the list position
of the failing value, and python -m json pretty-prints JSON from the command line (python -m json.tool still
works).
Checkpoint
Check: Types that JSON lacks
JSON numbers have no precision of their own
A JSON number is a run of digits with no size or precision attached, so what it means depends on the program that reads it:
import json
from decimal import Decimal
print(json.dumps([0.1 + 0.2, 10**20, float("nan"), float("inf")])) # NaN and Infinity are not JSON
try:
json.dumps({"ratio": float("nan")}, allow_nan=False) # strict JSON for other systems
except ValueError as err:
print("ValueError:", err)
print(json.loads('{"price": 19.99}')) # a float
print(json.loads('{"price": 19.99}', parse_float=Decimal)) # the exact decimal value
print(json.loads('{"x": 1, "x": 2}')) # a repeated key: the last one wins
big = 2**53 + 1 # an identifier just above 2**53
print(big, int(float(big))) # what a reader that stores numbers as 64-bit floats keeps Output
[0.30000000000000004, 100000000000000000000, NaN, Infinity]
ValueError: Out of range float values are not JSON compliant: nan
{'price': 19.99}
{'price': Decimal('19.99')}
{'x': 2}
9007199254740993 9007199254740992
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 json_numbers.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- Python writes a float with the shortest text that reads back as the same float, so
0.1 + 0.2appears as0.30000000000000004: the rounding happened in the arithmetic, not in JSON. - Python reads and writes integers of any size, but many programs read every JSON number as a 64-bit
IEEE 754: The standard for floating-point arithmetic that almost every computer follows. A Python float is its 64-bit double, a binary fraction with 53 significant bits and an exponent. More in “Floating-point surprises and how to handle them” float, which stores whole numbers exactly only up to 2⁵³, about nine million
billion. The last line shows what such a program keeps of
2**53 + 1: one less. Send large identifiers as strings. NaNandInfinityare not part of JSON, yetdumps()writes them by default, and other programs then fail to parse the file. Passallow_nan=Falseto get aValueErrorinstead, or replace them withnullyourself.parse_float=Decimalreads numbers with a fraction as exactDecimalvalues, which matters for money.- When an object repeats a key, the last value wins without a warning.
object_pairs_hookreceives every pair and can refuse duplicates.
Storing money as floats
json.loads('{"price": 19.99}') gives a binary float, and sums of such floats drift away from the exact amounts.
Send amounts as strings or as whole numbers of the smallest unit, such as cents, and read them with Decimal.
Checkpoint
Check: Numbers in JSON
Parsing proves the text is JSON, not that the data is right
JSON is data, never code: parse it with json.loads(), never with eval(), which would run whatever the text
contains. Parsing only tells you that the text is valid JSON. The result can still be a list where you expected an
object, a string where you expected a number, or a quantity of 5,000. Check the shape before you use it:
import json
def check_order(data):
"""Raise ValueError unless data has the shape an order must have."""
if not isinstance(data, dict):
raise ValueError("an order must be a JSON object")
if not isinstance(data.get("id"), int) or isinstance(data.get("id"), bool):
raise ValueError("id must be a whole number")
items = data.get("items")
if not isinstance(items, list) or not items:
raise ValueError("items must be a non-empty list")
for i, item in enumerate(items):
if not isinstance(item, dict) or not isinstance(item.get("sku"), str):
raise ValueError(f"items[{i}] needs a text sku")
qty = item.get("qty")
if not isinstance(qty, int) or isinstance(qty, bool) or not 1 <= qty <= 100:
raise ValueError(f"items[{i}].qty must be a whole number from 1 to 100")
return data
requests = [
'{"id": 7, "items": [{"sku": "PEN-01", "qty": 2}]}',
'[1, 2, 3]',
'{"id": "7", "items": [{"sku": "PEN-01", "qty": 2}]}',
'{"id": 8, "items": [{"sku": "PEN-01", "qty": true}]}',
'{"id": 9, "items": [{"sku": "PEN-01", "qty": 5000}]}',
'{"id": 10, "items": [{"sku": "PEN-01", "qty": 1}',
]
for text in requests:
try:
order = check_order(json.loads(text))
print("accepted order", order["id"])
except json.JSONDecodeError as err:
print("not JSON:", err.msg)
except ValueError as err:
print("refused:", err) Output
accepted order 7 refused: an order must be a JSON object refused: id must be a whole number refused: items[0].qty must be a whole number from 1 to 100 refused: items[0].qty must be a whole number from 1 to 100 not JSON: Expecting ',' delimiter
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 validate.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Each check names the field it refused, so the client learns what to fix. One check is less obvious: True and
False are instances of int in Python, so a test that only asks isinstance(qty, int) accepts "qty": true as a
quantity of 1. For larger formats, write the rules once as a
A JSON document that describes the shape other JSON must have, its types, required fields and ranges, so that a library can check data against it.
and check data against it with a schema library, and for input from the internet, limit its size first: the json
documentation warns that a malicious document can make the decoder use a lot of CPU time and memory.
Key idea
json.loads() proves only that the text is JSON: check the type, the fields and the ranges of what it returns
before any of it reaches your logic.
Checkpoint
Check: Checking what you load
Practise: store dates, decimals and sets
The exercise asks for two functions. to_json() writes datetimes as ISO 8601 text, Decimals as strings and sets as
sorted lists, at any depth, with sorted keys and non-ASCII text kept as it is; from_json() turns every string under
a key that ends in _at back into a datetime. Its sample tests run in your browser:
Exercise · Medium · Python
Store dates, decimals and sets in JSON
Write two functions in jsontypes.py.
to_json(obj)returns JSON text forobj, which may hold, at any depth inside dictionaries and lists, three types thatjson.dumps()cannot write by itself:datetime.datetimevalues, written as ISO 8601 text withisoformat();decimal.Decimalvalues, written as strings so that no digit is lost; and sets, written as sorted lists. Keys are sorted and non-ASCII characters are written as they are, so passsort_keys=Trueandensure_ascii=False. Any other value that JSON cannot store, such as adatetime.dateor a complex number, raisesTypeError.from_json(text)parses JSON text and turns every string whose key ends with_atback into adatetime, in objects at any depth. Everything else stays asjson.loads()gives it: a decimal comes back as a string, a set as a list, and anullunder an_atkey asNone.
Starter code · jsontypes.py
import json
def to_json(obj):
"""JSON text for obj; datetimes, Decimals and sets included."""
# Replace this line with your code.
return json.dumps(obj)
def from_json(text):
"""The data of text, with the *_at strings turned back into datetimes."""
# Replace this line with your code.
return json.loads(text) The sample tests · test_jsontypes.py
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from jsontypes import from_json, to_json
IST = timezone(timedelta(hours=5, minutes=30))
ORDER = {
"id": 501,
"amount": Decimal("1999.90"),
"tags": {"gift", "express"},
"created_at": datetime(2025, 3, 14, 9, 30, tzinfo=IST),
"lines": [{"sku": "PEN-01", "price": Decimal("49.50"), "shipped_at": datetime(2025, 3, 15, 18, 0)}],
}
def test_writes_the_three_types():
"""writes datetimes as ISO 8601, Decimals as strings and sets as sorted lists, keys sorted"""
assert to_json(ORDER) == (
'{"amount": "1999.90", "created_at": "2025-03-14T09:30:00+05:30", "id": 501, '
'"lines": [{"price": "49.50", "shipped_at": "2025-03-15T18:00:00", "sku": "PEN-01"}], '
'"tags": ["express", "gift"]}'
)
def test_keeps_non_ascii_text():
"""writes non-ASCII characters as they are"""
assert to_json({"name": "अनन्या", "city": "Zürich"}) == '{"city": "Zürich", "name": "अनन्या"}'
def test_other_types_still_fail():
"""raises TypeError for a date, a complex number or any other object"""
for value in [date(2025, 3, 14), 1 + 2j, object()]:
try:
to_json({"x": value})
except TypeError:
continue
assert False, f"to_json() should raise TypeError for {value!r}"
def test_reads_datetimes_back():
"""turns the *_at strings back into datetimes, at any depth, and leaves the rest alone"""
back = from_json(to_json(ORDER))
assert back["created_at"] == ORDER["created_at"]
assert back["lines"][0]["shipped_at"] == datetime(2025, 3, 15, 18, 0)
assert back["amount"] == "1999.90"
assert back["tags"] == ["express", "gift"]
assert back["lines"][0]["sku"] == "PEN-01"
def test_null_and_other_keys():
"""keeps None under an _at key, and strings under other keys"""
back = from_json('{"deleted_at": null, "format": "2025-03-14T09:30:00", "items": [{"paid_at": "2025-01-02T03:04:05+00:00"}]}')
assert back["deleted_at"] is None
assert back["format"] == "2025-03-14T09:30:00"
assert back["items"][0]["paid_at"] == datetime(2025, 1, 2, 3, 4, 5, tzinfo=timezone.utc) A hint
Give json.dumps() a function as default=: it is called with every value that dumps() cannot write, and it returns something that it can (a string or a list), or raises TypeError. For the way back, json.loads(text, object_hook=fix) calls fix(d) with every decoded object, innermost first, and uses what fix returns; change the values of the keys that end with "_at" with datetime.fromisoformat() when they are strings.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
The practice below covers types, files, readable output, custom types, numbers and validation. Your weakest idea comes first, then one question follows another, and each choice is explained the moment you pick it:
Practice that adapts to you
Practise this lesson
18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
What is the difference between json.load() and json.loads(), and between json.dump() and json.dumps()?
The functions with an s work on strings: json.loads(text) parses JSON text that is already in memory, such as the
body of an HTTP response, and json.dumps(obj) returns the JSON text as a str. The functions without the s work
on file objects: json.load(f) reads the JSON from an open file and parses it, and json.dump(obj, f) writes the JSON
text into an open file and returns nothing. They take the same formatting and conversion options, such as indent,
sort_keys, default and object_hook. Open the file with encoding="utf-8", and for load() read it in text
mode: json.loads() also accepts bytes, which it decodes as UTF-8, UTF-16 or UTF-32.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
An order service reads amounts such as 366.25 from JSON with json.loads(), computes 18 % tax on each and rounds it to two decimals. Over 100,000 orders its tax total differs from the payment provider's by 15.72. Explain why, and fix how amounts are read, calculated and written.
-
Mid to senior
An HTTP endpoint passes the request body straight to json.loads() and then into business logic. What can a buggy client or an attacker send that hurts the service, and which limits and checks do you add?
Key takeaways
loads()anddumps()convert between JSON text and Python objects;load()anddump()read from and write to files opened withencoding="utf-8".- JSON objects become dicts, arrays lists and
nullNone; tuples come back as lists and every key as a string. indent=2,sort_keys=Trueandensure_ascii=Falsemake files that people can read and that diff cleanly.default=stores types JSON lacks, such asdatetime(ISO 8601),Decimal(a string) andset(a sorted list);object_hookrestores them, but only when the reader knows what to expect.- Python writes
NaNandInfinityunlessallow_nan=False, a reader that uses 64-bit floats rounds integers above 2⁵³, andparse_float=Decimalreads exact decimals. - Parse with
json.loads(), nevereval(), and check the shape and the ranges of what you loaded before using it.
Terms in this lesson
- round trip
- Turning data into another form and back, such as a dict into JSON text and back again. A round trip that loses nothing gives an equal value; through JSON, tuples come back as lists and keys as strings.
- serialise
- To write data in a format that can be stored or sent, such as a dict as JSON text with json.dumps(); reading it back is deserialising. Only the types the format knows come back without help.
References
- json: JSON encoder and decoder (Python Software Foundation)
- The Python Tutorial: saving structured data with json (Python Software Foundation)
- json: command-line interface (Python Software Foundation)
- What's New In Python 3.14: json (Python Software Foundation)
- The Python Tutorial: floating-point arithmetic, issues and limitations (Python Software Foundation)
- datetime.isoformat() and fromisoformat() (Python Software Foundation)
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format (Internet Engineering Task Force)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress