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

6 May 2026 6 min read

Last updated: 24 September 2026

Share:

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

python
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)
text
3 True None
True

The 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

python
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)
text
{'id': [1, 2], '7': 'seven', 'tags': ['a', 'b']}
TypeError: Object of type set is not JSON serializable

JSON 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

OptionEffectUse it for
indent=2One value per line, indentedConfiguration files, debugging, anything people read
sort_keys=TrueKeys in alphabetical orderFiles kept in Git, so diffs stay small
separators=(",", ":")No spaces at allCompact payloads over the network
ensure_ascii=FalseKeep non-ASCII characters as they areReadable 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):

python
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))
text
{"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:

python
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"])
text
0.30000000000000004 0.3

When parsing fails

python
broken = '{"order_id": 1042, "city": "Pune",}'
try:
    json.loads(broken)
except json.JSONDecodeError as exc:
    print(exc.msg, "| line", exc.lineno, "column", exc.colno)
text
Expecting property name enclosed in double quotes | line 1 column 35

JSONDecodeError 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

python
print(json.dumps({"ratio": float("nan")}))
try:
    json.dumps({"ratio": float("nan")}, allow_nan=False)
except ValueError as exc:
    print("ValueError:", exc)
text
{"ratio": NaN}
ValueError: Out of range float values are not JSON compliant

By 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:

python
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])
text
['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_case or camelCase, 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" or 134910 paise, 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?

load reads from an open file object; loads parses a string. dump and dumps are the writing equivalents.

Why are my dictionary's integer keys strings after loading?

JSON object keys are always strings, so json.dumps converts integer keys and json.loads returns them as strings. Convert them back explicitly if you need integers.

How do I pretty-print a JSON file quickly?

From a terminal, python -m json.tool data.json prints it indented, and reports the line and column of the first syntax error if the file is invalid.

Is the json module fast enough?

For most programs, yes. When encoding or decoding large volumes is a measured bottleneck, third-party libraries such as orjson are considerably faster and serialise more types, such as datetimes, without a default function.

Is JSON a good format for configuration?

It works, but it has no comments. For configuration that people edit, TOML, which Python reads with the built-in tomllib module since 3.11, is often more comfortable.

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

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.

Explore course