Working with JSON in Python: A Practical Guide
A practical guide to JSON in Python with the json module: how Python types map to JSON and what does not survive a round trip, reading and writing files, pretty and compact output, serialising dates and Decimals, keeping money exact when parsing, understanding decode errors, JSON Lines for large data, and the NaN trap.
GloryTecks Team
Training & Placement Faculty
Last updated: 24 September 2026
JSON is how most programs talk to each other: API responses, configuration files, messages on a queue, logs. Python's standard library handles it with one module, json, and four functions do almost everything (Python docs: json). The functions are simple. The traps are in the edges: types JSON does not have, numbers that lose precision, and values Python writes that other languages refuse to read.
Strings and files: loads, dumps, load, dump
import json
from pathlib import Path
payload = '{"order_id": 1042, "city": "Hyderabad", "items": [{"sku": "PEN-01", "qty": 3}], "paid": true, "coupon": null}'
order = json.loads(payload)
print(order["items"][0]["qty"], order["paid"], order["coupon"])
Path("order.json").write_text(json.dumps(order, indent=2), encoding="utf-8")
with open("order.json", encoding="utf-8") as f:
print(json.load(f) == order)3 True None
TrueThe s stands for string: loads parses a JSON string and dumps produces one, while load and dump work with open files. JSON's true, false and null become Python's True, False and None, objects become dictionaries and arrays become lists.
What does not survive a round trip
original = {"id": (1, 2), 7: "seven", "tags": ["a", "b"]}
back = json.loads(json.dumps(original))
print(back)
try:
json.dumps({"seen": {"a", "b"}})
except TypeError as exc:
print("TypeError:", exc){'id': [1, 2], '7': 'seven', 'tags': ['a', 'b']}
TypeError: Object of type set is not JSON serializableJSON has fewer types than Python (RFC 8259). Tuples come back as lists, integer keys come back as strings (JSON object keys are always strings), and sets, dates, Decimal values and custom objects cannot be encoded at all without help. Code that reads its own JSON back must convert these deliberately.
Pretty, compact and readable output
| Option | Effect | Use it for |
|---|---|---|
indent=2 | One value per line, indented | Configuration files, debugging, anything people read |
sort_keys=True | Keys in alphabetical order | Files kept in Git, so diffs stay small |
separators=(",", ":") | No spaces at all | Compact payloads over the network |
ensure_ascii=False | Keep non-ASCII characters as they are | Readable text such as "Bengaluru – ₹" instead of \u escapes |
When writing non-ASCII text to a file with ensure_ascii=False, open the file with encoding="utf-8".
Dates, Decimals and other types: default=
Pass a function as default, and dumps calls it for any object it cannot encode (Python docs: json):
from datetime import datetime, timezone
from decimal import Decimal
def to_json(value):
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, Decimal):
return str(value) # exact, as a string
if isinstance(value, set):
return sorted(value)
raise TypeError(f"cannot serialise {type(value).__name__}")
invoice = {"total": Decimal("1349.10"), "issued": datetime(2026, 9, 25, 9, 30, tzinfo=timezone.utc), "tags": {"gst", "paid"}}
print(json.dumps(invoice, default=to_json)){"total": "1349.10", "issued": "2026-09-25T09:30:00+00:00", "tags": ["gst", "paid"]}ISO 8601 strings with an offset are the conventional way to carry timestamps in JSON; datetime in Python shows how to parse them back with datetime.fromisoformat. Raising TypeError for anything unexpected keeps the function honest, rather than silently writing something unreadable.
Keeping money exact when parsing
JSON numbers with decimals are parsed as Python floats by default, which cannot represent most decimal fractions exactly. For money, ask for Decimal instead:
text = '{"price": 0.1, "qty": 3}'
as_float = json.loads(text)
as_decimal = json.loads(text, parse_float=Decimal)
print(as_float["price"] * as_float["qty"], as_decimal["price"] * as_decimal["qty"])0.30000000000000004 0.3When parsing fails
broken = '{"order_id": 1042, "city": "Pune",}'
try:
json.loads(broken)
except json.JSONDecodeError as exc:
print(exc.msg, "| line", exc.lineno, "column", exc.colno)Expecting property name enclosed in double quotes | line 1 column 35JSONDecodeError is a subclass of ValueError and carries the position of the problem, which makes it easy to report which file and line are broken. The trailing comma above is valid in JavaScript and in Python dictionaries but not in JSON, a common cause of failure in hand-edited files; single quotes around strings are the other.
The NaN trap
print(json.dumps({"ratio": float("nan")}))
try:
json.dumps({"ratio": float("nan")}, allow_nan=False)
except ValueError as exc:
print("ValueError:", exc){"ratio": NaN}
ValueError: Out of range float values are not JSON compliantBy default Python writes NaN and Infinity, which strict JSON forbids, so parsers in other languages reject the file. Pass allow_nan=False when writing JSON for other systems, and replace missing numbers with null before encoding.
Large data: JSON Lines
A single JSON array must be read whole before any of it can be used. For logs, exports and data pipelines, JSON Lines puts one JSON value on each line (JSON Lines), so a file can be written incrementally and read one record at a time:
events = [{"id": 1, "type": "login"}, {"id": 2, "type": "purchase"}]
with open("events.jsonl", "w", encoding="utf-8") as f:
for e in events:
f.write(json.dumps(e) + "\n")
with open("events.jsonl", encoding="utf-8") as f:
print([json.loads(line)["type"] for line in f])['login', 'purchase']Reading JSON from an API
Most JSON arrives in HTTP responses. Check the response before trusting its body: confirm the status code is a success, since many APIs return an error message as JSON with a 4xx or 5xx status, and confirm the content type is JSON before parsing. HTTP libraries such as Requests offer a response.json() shortcut that parses the body for you. Then read nested fields defensively. order.get("coupon") returns None when a key is absent instead of raising KeyError, and a chain such as data.get("customer", {}).get("city") tolerates a missing parent object. Decide which fields are required; for those, a clear error naming the missing field beats a silent None travelling through your program.
Designing JSON other people will read
When your program produces JSON for other systems, a few conventions prevent most integration problems:
- Pick one key style and keep it:
snake_caseorcamelCase, everywhere. - Write timestamps as ISO 8601 strings with an offset, never as local times without a zone.
- Send money as a string or as integer minor units, such as
"1349.10"or134910paise, so no system rounds it through a float. - Distinguish null from missing deliberately, and document which fields may be null.
- Prefer an array of objects over parallel arrays, so each record stays together.
- Add fields rather than changing them. Consumers usually ignore new keys, but renaming or retyping one breaks them.
Validating what arrives
Parsing only proves the text is valid JSON, not that it has the fields and types your code expects. For data from outside, such as API payloads, validate the structure immediately after parsing: Pydantic explained turns a class definition into exactly that check, with clear errors for every missing or wrong field.
What is the difference between json.load and json.loads?
Why are my dictionary's integer keys strings after loading?
How do I pretty-print a JSON file quickly?
Is the json module fast enough?
Is JSON a good format for configuration?
JSON is one of the file formats in Python file handling; for flat, tabular data, CSV is often simpler. CSV and JSON handling are part of the file handling module of the Python Programming course.
Sources
- json — JSON encoder and decoder — Python documentation
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format — RFC Editor
- JSON Lines — JSON Lines
GloryTecks Team
Training & Placement Faculty
The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.
Want to master Python?
Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.
Related articles
Learn Python from Scratch: A Complete 2026 Roadmap
How to learn Python from zero: install a current version (including the new Windows install manager), then work through seven stages, from values and loops to files, virtual environments, classes and tests, each ending with a small program you should be able to write on your own.
Python Interview Questions: Top 50 with Answers
Fifty conceptual Python interview questions with short, precise answers, grouped by topic: basics, types and mutability, data structures, functions and scope, OOP, iterators and decorators, errors and files, and internals, with runnable examples for the ones people most often get wrong.
Python for Data Science: The Essential Libraries
The Python data science stack explained by the job each library does: NumPy for arrays, pandas for tables, Matplotlib and seaborn for charts, SciPy and statsmodels for statistics, scikit-learn for models and Jupyter for exploration, with one runnable example that passes a real dataset through them.