🐍 Python

Python json Module — Complete Guide

A complete, professional guide to Python's json module — dumps/loads, dump/load, Python-JSON type mapping, serializing custom objects, custom encoders and decoders, and error handling.

📅

Last Updated

March 2026

⏱️

Read Time

27 min

🎯

Level

Intermediate

What is JSON, and What is Python's json Module?

JSON (JavaScript Object Notation) is a lightweight, text-based data format used to represent structured data — objects, arrays, strings, numbers, booleans, and null — in a form that's both human-readable and easy for machines to parse. Despite its name referencing JavaScript, JSON is a completely language-independent format, and it has become the de facto standard for exchanging data between web APIs, configuration files, and countless data storage and messaging systems.

Python's built-in json module, part of the standard library since Python 2.6, provides the tools to convert between Python's native data structures (dictionaries, lists, strings, numbers, booleans, None) and JSON-formatted text. This conversion process has two directions, each with its own standard terminology: converting a Python object into a JSON string is called serialization (or "encoding"), and converting a JSON string back into a Python object is called deserialization (or "decoding").

Because JSON is so close in structure to Python's own dictionaries and lists, working with it feels almost seamless — a JSON object maps naturally to a Python dict, a JSON array maps to a Python list, and JSON's string, number, boolean, and null types map directly to Python's str, int/float, bool, and None. This close structural similarity is exactly why the json module needs comparatively little configuration for the vast majority of everyday use cases.

The Four Core Functions of the json Module

Nearly everything you do with the json module boils down to one of four functions, distinguished by direction (encode vs decode) and target (string vs file).

FunctionDirectionWorks WithTypical Use
json.dumps(obj)Python → JSONReturns a JSON stringSending data over a network, embedding JSON in a log line
json.loads(s)JSON → PythonTakes a JSON string, returns a Python objectParsing an API response body
json.dump(obj, file)Python → JSONWrites JSON directly to an open file objectSaving configuration or data to a .json file
json.load(file)JSON → PythonReads JSON directly from an open file objectLoading configuration or data from a .json file

The naming convention is consistent and easy to remember: functions ending in -s (dumps, loads) work with strings in memory; functions without the trailing s (dump, load) work directly with file objects, reading or writing the JSON text as a stream rather than building the entire string in memory first.

Python ↔ JSON Type Mapping

JSON has a smaller, simpler type system than Python. Understanding exactly how each Python type maps to (and back from) its JSON equivalent avoids surprises — particularly for types JSON has no direct concept of at all.

Python TypeJSON EquivalentRound-trips Back to Same Type?
dictobject ({...})✅ Yes
listarray ([...])✅ Yes
tuplearray ([...])❌ No — becomes a list after decoding
strstring✅ Yes
intnumber✅ Yes
floatnumber✅ Yes
Truetrue✅ Yes
Falsefalse✅ Yes
Nonenull✅ Yes
set❌ Not supported — raises TypeErrorN/A
datetime❌ Not supported — raises TypeErrorN/A
Custom class instance❌ Not supported by default — raises TypeErrorN/A

The tuple-to-list conversion deserves particular attention: because JSON has no separate concept of a tuple, Python tuples are silently serialized as ordinary JSON arrays, and when decoded back, they come back as a list, not a tuple — the original type information is permanently lost in that round trip. Similarly, sets, datetime objects, and instances of your own custom classes have no native JSON representation at all, and attempting to serialize them directly raises a TypeError unless you provide custom handling (covered later in this guide).

Serializing Python to JSON with json.dumps()

json.dumps() converts a Python object into a JSON-formatted string. Beyond the basic conversion, it accepts several useful keyword arguments controlling exactly how the output is formatted.

🐍 Pythonjson_dumps_basics.py
import json

user = {
    "name": "Ashish",
    "age": 28,
    "is_active": True,
    "skills": ["Python", "JSON", "Web Development"],
    "address": None
}

compact = json.dumps(user)
print(compact)
# {"name": "Ashish", "age": 28, "is_active": true, "skills": ["Python", "JSON", "Web Development"], "address": null}

pretty = json.dumps(user, indent=4, sort_keys=True)
print(pretty)

Output

{ "address": null, "age": 28, "is_active": true, "name": "Ashish", "skills": [ "Python", "JSON", "Web Development" ] }
ParameterPurposeExample
indentPretty-print with N spaces per nesting leveljson.dumps(data, indent=2)
sort_keysAlphabetically sort object keys in the outputjson.dumps(data, sort_keys=True)
separatorsCustomize the item/key separators (default: ', ' and ': ')json.dumps(data, separators=(',', ':'))
ensure_asciiEscape all non-ASCII characters as \uXXXX (default True)json.dumps(data, ensure_ascii=False)
defaultProvide a fallback function for non-serializable objectsjson.dumps(data, default=str)

The separators=(',', ':') combination (removing the default spaces) is commonly used to produce the most compact JSON possible — useful when minimizing payload size matters, such as for network transmission — while indent and sort_keys together produce the most readable, diff-friendly output, ideal for configuration files or debugging logs.

Deserializing JSON to Python with json.loads()

json.loads() performs the reverse operation — parsing a JSON-formatted string and returning the equivalent Python object, typically a dict or list at the top level, with nested structures converted recursively.

🐍 Pythonjson_loads_basics.py
import json

json_text = '''
{
    "name": "Ashish",
    "age": 28,
    "skills": ["Python", "JSON"],
    "is_active": true
}
'''

data = json.loads(json_text)
print(type(data))       # <class 'dict'>
print(data["name"])     # Ashish
print(data["skills"])   # ['Python', 'JSON']
print(type(data["is_active"]))   # <class 'bool'>

Notice that the JSON boolean true correctly becomes the Python True, and the JSON structure's nesting is fully preserved as native Python dicts and lists — no manual recursive parsing is ever required, since json.loads() handles arbitrarily deep nesting automatically.

Serialization and Deserialization Flow

This flowchart traces the complete round trip of data moving from a Python object, through JSON text, and potentially back again — the exact process behind sending and receiving data with a web API.

🐍 Python objectdict, list, str, int, etc.
serialize
⚙️ json.dumps()Recursively walks the structure
checks
❓ All values JSON-serializable?dict/list/str/int/float/bool/None?
yes
📄 JSON string producedReady to send/store
sent
🚫 TypeError raisedNon-serializable type found
🌐 Transmitted / StoredNetwork request, file, database
received
⚙️ json.loads()Parses the JSON text
deserialize
🐍 Python object (rebuilt)dict/list structure restored

Code Execution Flow — from source to output

This round trip isn't always perfectly symmetrical: as covered earlier, a Python tuple serialized via dumps() comes back from loads() as a list, not a tuple — the JSON format itself has no way to preserve that original Python-specific type distinction.

Reading and Writing JSON Files with dump() and load()

When working with JSON stored in a file rather than a string already in memory, json.dump() and json.load() operate directly on an open file object, avoiding the need to manually build or parse a string yourself.

🐍 Pythonjson_file_operations.py
import json

config = {
    "app_name": "Tech Sustainify",
    "version": "2.1.0",
    "debug": False,
    "max_connections": 100
}

# Writing JSON to a file
with open("config.json", "w") as f:
    json.dump(config, f, indent=4)

# Reading JSON from a file
with open("config.json", "r") as f:
    loaded_config = json.load(f)

print(loaded_config["app_name"])   # Tech Sustainify
print(loaded_config == config)     # True

Using json.dump()/json.load() directly with a file, rather than manually combining json.dumps() with file.write() (or json.loads() with file.read()), is both more concise and slightly more memory-efficient for very large JSON files, since the file-based versions can stream data rather than requiring the entire JSON text to exist as one string in memory at once.

Serializing Custom Objects and Non-Standard Types

By default, the json module only knows how to serialize the handful of built-in types listed in the mapping table earlier. Attempting to serialize a datetime, a set, or an instance of your own class raises a TypeError. There are two standard ways to teach json how to handle these cases.

Option 1 — The default Parameter (Simple Cases)

🐍 Pythoncustom_default_function.py
import json
from datetime import datetime

def json_default(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, set):
        return list(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

data = {
    "created_at": datetime(2026, 3, 16, 10, 30),
    "tags": {"python", "json", "tutorial"}
}

print(json.dumps(data, default=json_default))
# {"created_at": "2026-03-16T10:30:00", "tags": ["python", "json", "tutorial"]}

Option 2 — A Custom JSONEncoder Subclass (Reusable Cases)

🐍 Pythoncustom_json_encoder.py
import json
from datetime import datetime

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        if isinstance(obj, set):
            return list(obj)
        return super().default(obj)

data = {"created_at": datetime.now(), "tags": {"a", "b"}}
print(json.dumps(data, cls=CustomEncoder, indent=2))

The default parameter approach is quick and convenient for one-off scripts, while subclassing json.JSONEncoder and passing it via cls=CustomEncoder is better suited to larger applications where the same custom serialization logic needs to be reused consistently across many different dumps()/dump() calls.

Custom Decoding with object_hook

Just as serialization can be customized, deserialization can too. The object_hook parameter lets you intercept every JSON object as it's being decoded and transform it into something other than a plain dict — commonly, an instance of a custom class.

🐍 Pythonobject_hook_example.py
import json

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age})"

def user_decoder(d):
    if "name" in d and "age" in d:
        return User(d["name"], d["age"])
    return d

json_text = '{"name": "Ashish", "age": 28}'
user = json.loads(json_text, object_hook=user_decoder)
print(user)                # User(name='Ashish', age=28)
print(type(user))          # <class '__main__.User'>

object_hook is called on every JSON object encountered during parsing, from the innermost nested objects outward — which is why the function must check whether the specific keys it cares about are present before deciding to convert, and return the dictionary unchanged (return d) for any object that doesn't match the expected shape.

Handling JSON Errors — JSONDecodeError

Malformed or invalid JSON text raises a json.JSONDecodeError (a subclass of ValueError) when passed to json.loads() or json.load(). This exception carries useful positional information pinpointing exactly where the parsing failed.

🐍 Pythonjson_error_handling.py
import json

invalid_json = '{"name": "Ashish", "age": }'   # missing value after 'age':

try:
    data = json.loads(invalid_json)
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e.msg}")
    print(f"At line {e.lineno}, column {e.colno} (character {e.pos})")

# Output:
# Invalid JSON: Expecting value
# At line 1, column 27 (character 26)
  • e.msg — A short, human-readable description of what went wrong.

  • e.lineno / e.colno — The line and column number where the error occurred, ideal for showing a helpful message pointing to a specific spot in a config file.

  • e.pos — The absolute character index into the original string where parsing failed.

  • Since JSONDecodeError is a subclass of ValueError, code that only needs a generic catch can simply write except ValueError: instead, though catching the more specific type gives access to these extra positional attributes.

json vs pickle — Which Should You Use?

Python offers a second built-in serialization tool, pickle, which serves a fundamentally different purpose than json. This comparison clarifies when each is appropriate.

Aspectjsonpickle
FormatHuman-readable textBinary, Python-specific
Cross-language compatible?✅ Yes — universal, language-independent❌ No — Python only
Can serialize arbitrary Python objects?❌ No — only basic types by default✅ Yes — nearly any Python object
Security when loading untrusted data✅ Safe — data-only, no code execution❌ Unsafe — can execute arbitrary code
Typical use caseAPIs, config files, cross-system data exchangeCaching, saving Python-specific objects internally
Human-editable?✅ Yes — plain text, easy to read/edit❌ No — opaque binary format
Preserves exact Python types (tuple, set, etc.)?❌ No — tuples become lists, sets unsupported✅ Yes — preserves nearly everything exactly

The single most important distinction is security: unpickling data from an untrusted or unauthenticated source is a well-documented remote code execution risk, because pickle's format can encode instructions to reconstruct arbitrary Python objects, including ones that execute code as a side effect of being loaded. json, by contrast, only ever produces plain data structures — parsing untrusted JSON cannot execute code, making it the safe default choice for anything involving external or network-sourced data.

The json Module's Processing Pipeline

Understanding the json module as a layered pipeline — from raw Python objects down to text, and back — clarifies exactly where each customization hook (default, cls, object_hook) plugs in.

Python Objects
dict, list, str, int, float, bool, NoneCustom classes (need default/cls)
Encoding Layer
json.dumps() / json.dump()JSONEncoder (default, cls parameters)
JSON Text
Human-readable stringStored in files, sent over HTTP
Decoding Layer
json.loads() / json.load()JSONDecoder (object_hook parameter)
Python Objects (Rebuilt)
dict/list structure restoredOptionally custom classes via object_hook

Architecture Diagram

Practice: A Small JSON-Based Config Loader

This example combines file operations, error handling, and default values into a realistic configuration loader.

🐍 Pythonconfig_loader.py
import json

DEFAULT_CONFIG = {"debug": False, "max_retries": 3, "timeout": 30}

def load_config(path):
    try:
        with open(path, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        print(f"Config file '{path}' not found — using defaults.")
        return DEFAULT_CONFIG
    except json.JSONDecodeError as e:
        print(f"Config file is invalid JSON ({e.msg} at line {e.lineno}) — using defaults.")
        return DEFAULT_CONFIG

def save_config(path, config):
    with open(path, "w") as f:
        json.dump(config, f, indent=4, sort_keys=True)

config = load_config("app_settings.json")
print(config)
config["debug"] = True
save_config("app_settings.json", config)

Output

Config file 'app_settings.json' not found — using defaults. {'debug': False, 'max_retries': 3, 'timeout': 30}

Practice This Code — Live Editor

Python json Module Best Practices

These practices help you use the json module safely, efficiently, and predictably in real applications.

  • Prefer dump()/load() over manually combining dumps()/loads() with file I/O — The file-based functions are more concise and avoid holding the entire JSON string in memory at once for large files.

  • Always catch json.JSONDecodeError around untrusted or external JSON input — Any data coming from a network request, uploaded file, or user input can be malformed; handle the error gracefully rather than letting the program crash.

  • Never use pickle for untrusted data — use json instead — pickle can execute arbitrary code during deserialization; json is data-only and safe to parse from any source.

  • Remember tuples become lists after a round trip — If exact type preservation matters for tuples or sets, either convert explicitly on both ends or use pickle instead (for trusted, Python-only contexts).

  • Use indent and sort_keys for anything a human will read or diff — Configuration files and debug output benefit enormously from consistent, readable formatting; compact output is better reserved for network payloads.

  • Use a custom JSONEncoder subclass for reusable custom serialization logic — The default parameter is fine for one-off scripts, but a reusable CustomEncoder class keeps serialization logic centralized across a larger codebase.

  • Validate decoded data before trusting its structure — json.loads() will happily return a dict missing keys your code expects; check for required keys explicitly rather than assuming the shape of external JSON.

  • Set ensure_ascii=False when working with non-English text you intend to keep readable — The default escapes all non-ASCII characters as \uXXXX sequences, which is safe but not human-readable in the raw output.

Advantages and Disadvantages of the json Module

The json module is fast, safe, and universally compatible, but it has real, deliberate limitations compared to Python-specific serialization tools.

✅ Advantages
No Installation RequiredPart of the standard library, available immediately with a single import in every Python installation.
Universally Compatible FormatJSON is language-independent, making it the natural choice for data exchanged between Python and any other system — web APIs, JavaScript frontends, mobile apps, and more.
Safe to Parse Untrusted InputUnlike pickle, JSON parsing only ever produces plain data structures and cannot execute arbitrary code, making it safe for network-sourced or user-submitted data.
Human-Readable and EditableJSON text can be opened, read, and hand-edited in any text editor, unlike binary serialization formats.
Extensible via default/object_hookCustom types can be supported with a small, well-defined amount of extra code, without needing a different library entirely.
❌ Disadvantages
Limited Native Type SupportOnly dict, list, str, int, float, bool, and None are supported directly; everything else (datetime, set, custom classes) requires manual handling.
Tuples Silently Become ListsThe round trip through JSON loses the distinction between Python tuples and lists, which can surprise code that relies on tuple-specific behaviour.
No Built-in Support for CommentsUnlike some configuration formats (YAML, TOML), standard JSON has no comment syntax at all, making annotated config files awkward.
Larger Payloads Than Binary FormatsAs a text-based format, JSON is generally less compact than binary alternatives like pickle or protocol buffers for the same data.
No Native Numeric Precision GuaranteesJSON numbers don't distinguish between int and float the way Python does at the type level, and very large integers can lose precision when processed by non-Python JSON parsers.

Python json Module — Interview Questions

Frequently asked interview questions about the json module, serialization, and common pitfalls.

Practice Questions — Test Your Understanding

Work through these practice questions on the json module. Try to predict the output or outcome before checking the answer.

1. What is the type of json.loads('[1, 2, 3]')?

Easy

2. What does json.loads(json.dumps((1, 2, 3))) return, and what type is it?

Medium

3. What happens when you run json.dumps({1: 'a', 2: 'b'})?

Medium

4. Why does json.dumps(datetime.now()) raise a TypeError?

Easy

5. If a JSON file contains a trailing comma, like {"a": 1, "b": 2,}, what happens when you call json.load() on it?

Medium

6. What is the purpose of setting ensure_ascii=False in json.dumps()?

Hard

7. Why might you use object_hook when calling json.loads() on API response data?

Hard

8. What is the safest way to handle JSON data received from an untrusted external source (e.g. a public API)?

Medium

Conclusion — Working with JSON in Python

The json module is one of the most frequently used tools in the entire Python standard library, precisely because JSON itself sits at the center of how modern systems exchange data — web APIs, configuration files, message queues, and countless data storage formats all speak JSON. The module's four core functions, dumps, loads, dump, and load, cover the overwhelming majority of real use cases with almost no configuration required at all.

The habits worth carrying forward: always handle JSONDecodeError around any JSON coming from outside your own program, never reach for pickle when the data might come from an untrusted source, remember that tuples become lists and several common Python types (datetime, set, custom classes) need explicit handling via default or a custom JSONEncoder, and use indent and sort_keys generously for anything a human being will ever need to read.

SituationRecommended Approach
Sending/receiving JSON over a network✅ json.dumps() / json.loads()
Reading/writing a JSON config file✅ json.dump() / json.load()
Serializing datetime, set, or custom objects✅ default parameter or custom JSONEncoder
Parsing potentially malformed JSON✅ Wrap in try/except json.JSONDecodeError
Data must remain Python-only, any type allowed⚠️ Consider pickle instead — trusted sources only
Data comes from an untrusted/external source✅ json — never pickle
Output needs to be human-readable✅ indent + sort_keys

From here, natural next topics include working with REST APIs using the requests library (which integrates tightly with JSON for both request and response bodies), dataclasses and JSON serialization patterns for cleanly converting between typed Python objects and JSON, and JSON Schema validation for enforcing structure on JSON data before your program relies on it.

Frequently Asked Questions (FAQ)