Work with JSON data
Learn to work with JSON data using the json module in Python. This lesson covers core concepts, step-by-step operations, and practical exercise.
Focus: work with json data using the json module
You've built a Python script that scrapes data from an API or collects user settings, but now you need to save it so you can load it back later. Simply writing a Python dictionary to a file won't work for other programs or even future versions of your own code. This is where JSON comes in — it's the universal, language-independent format for data exchange, and Python's json module makes parsing it dead simple. By the end of this lesson, you'll confidently convert between Python objects and JSON to read, write, and manipulate structured data in any project.
The problem this lesson solves
Imagine you're building a configuration manager. Python variables like {"theme": "dark", "language": "en"} vanish when the program ends. You could use pickle to serialize Python objects, but that format is Python-only, insecure, and unreadable. JSON (JavaScript Object Notation) solves this by providing a text-based, human-readable, and cross-platform serialization format. Every modern language — Python, JavaScript, Go, Rust — can parse JSON. The json module bridges your Python objects and this universal format without you needing to write a single parser.
Core concept / mental model
Think of JSON as a universal translator between Python and the outside world. When you call json.dumps(), Python converts a dictionary or list into a JSON string that looks like a JavaScript object. When you call json.loads(), it does the reverse: it reads a JSON string and rebuilds a Python dictionary or list. The two core methods are:
json.dumps(obj)— Serialize Python object → JSON string (dump to string).json.loads(str)— Deserialize JSON string → Python object (load from string).
For files, use json.dump(obj, file) and json.load(file) (notice no 's') — these directly read from / write to file handles.
Key type mappings
| Python | JSON | Example |
|---|---|---|
dict |
Object | {"key": "value"} |
list |
Array | [1, 2, 3] |
str |
String | "hello" |
int / float |
Number | 42, 3.14 |
bool |
true / false |
True → true |
None |
null |
None → null |
Pro Tip: Python tuples become JSON arrays; you lose the tuple type during conversion. If you need to preserve types, use a custom encoder or
pickle.
How it works step by step
Let's walk through the complete workflow of saving and loading a settings dictionary.
Step 1: Import the json module
import json
Step 2: Prepare your Python data
settings = {
"app_name": "MyNotepad",
"theme": "dark",
"language": "en",
"font_size": 14,
"plugins": ["spellcheck", "markdown"],
"auto_save": True,
"max_recent_files": None
}
Step 3: Serialize to JSON string (for display or transmission)
json_string = json.dumps(settings, indent=2)
print(json_string)
Output:
{
"app_name": "MyNotepad",
"theme": "dark",
"language": "en",
"font_size": 14,
"plugins": ["spellcheck", "markdown"],
"auto_save": true,
"max_recent_files": null
}
Notice: True became true and None became null — JSON's valid tokens.
Step 4: Write to a file
with open("settings.json", "w", encoding="utf-8") as f:
json.dump(settings, f, indent=4)
Why
indent? Without it,json.dumpproduces one long line.indent=2or4makes the file human-readable.
Step 5: Read back from the file
with open("settings.json", "r", encoding="utf-8") as f:
loaded_settings = json.load(f)
print(loaded_settings["theme"]) # 'dark'
This is the core loop: dict → json.dump → file → json.load → dict.
Hands-on walkthrough
Let's combine all steps into a real mini-project: a contact book that saves to JSON.
Example 1: Basic CRUD with JSON
import json
# Initial contacts data
contacts = [
{"name": "Alice", "phone": "555-1234", "email": "alice@example.com"},
{"name": "Bob", "phone": "555-5678", "email": "bob@example.com"}
]
# Save to file
with open("contacts.json", "w") as f:
json.dump(contacts, f, indent=2)
print("Contacts saved!")
# Add a new contact
new_contact = {"name": "Charlie", "phone": "555-9012", "email": "charlie@example.com"}
contacts.append(new_contact)
# Overwrite the file with updated list
with open("contacts.json", "w") as f:
json.dump(contacts, f, indent=2)
print("Updated contacts saved!")
# Load and display all names
with open("contacts.json", "r") as f:
loaded = json.load(f)
for contact in loaded:
print(f"{contact['name']} — {contact['phone']}")
Expected output:
Contacts saved!
Updated contacts saved!
Alice — 555-1234
Bob — 555-5678
Charlie — 555-9012
Example 2: Pretty printing and sorting keys
By default json.dumps outputs keys in insertion order (Python 3.7+). For deterministic output, use sort_keys=True.
data = {"c": 3, "a": 1, "b": 2}
print(json.dumps(data, indent=2, sort_keys=True))
Output:
{
"a": 1,
"b": 2,
"c": 3
}
Example 3: Handling custom objects (JSONEncoder)
Python's datetime objects are not JSON-serializable by default. You must convert them manually.
from datetime import datetime
def datetime_serializer(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Type {type(obj)} not serializable")
event = {
"title": "Python workshop",
"date": datetime(2024, 6, 15, 14, 30)
}
json_str = json.dumps(event, default=datetime_serializer, indent=2)
print(json_str)
Output:
{
"title": "Python workshop",
"date": "2024-06-15T14:30:00"
}
Compare options / when to choose what
| Approach | When to use | Pros | Cons |
|---|---|---|---|
json.dump/load |
File I/O (configs, data storage) | Simple, direct to disk | Requires file handle |
json.dumps/loads |
Network, in-memory, printing | Works on strings, flexible | Must manage encoding yourself |
pickle |
Internal Python tools | Supports all Python objects | Insecure, not human-readable |
yaml (PyYAML) |
Complex configs (e.g., pip, Ansible) | Supports comments, multiline | Slower, requires external lib |
When to pick JSON over YAML? JSON is faster, built-in, and universally supported — perfect for APIs and simple configs. Use YAML when you need comments or multi-line strings.
Troubleshooting & edge cases
Common mistakes
- Forgetting
ensure_ascii=False– When dumping non-ASCII characters (like emoji or Chinese), the defaultensure_ascii=Trueescapes them to\uXXXX. Passensure_ascii=Falseto preserve readability. ```python # Bad: "I ❤ Python" becomes "I \u2764 Python" print(json.dumps({"text": "I ❤ Python"})) # "I \u2764 Python"
# Good:
print(json.dumps({"text": "I ❤ Python"}, ensure_ascii=False)) # "I ❤ Python"
``
- **Writing multiple JSON objects without an array** – Eachjson.dumpcall writes one object. Writing five calls creates a file that's invalid JSON (concatenated objects). Always collect objects into a list first.
- **Usingjson.load()on a malformed file** – Always handlejson.JSONDecodeError`. Wrap in try/except.
Edge case: Empty data
empty_dict = json.loads("{}") # Returns empty dict {}
empty_list = json.loads("[]") # Returns empty list []
print(json.dumps(None)) # "null"
Edge case: Duplicate keys (ill-formed JSON)
Python's JSON parser by default keeps the last value for duplicate keys:
data = '{"name": "Alice", "name": "Bob"}'
print(json.loads(data)) # {'name': 'Bob'}
Fix: Use json.loads(data, object_pairs_hook=collections.OrderedDict) to preserve order and detect duplicates.
Error: TypeError after dumps with custom objects
# This will raise: TypeError: Object of type set is not JSON serializable
set_data = {"my_set": {1, 2, 3}}
json.dumps(set_data)
Solution: Convert sets to lists before serialization: {"my_list": list({1,2,3})}.
What you learned & what's next
You've mastered the core duo — json.dumps/loads and json.dump/load — enabling you to:
- Serialize Python dictionaries, lists, strings, numbers, booleans, and None to JSON
- Write JSON to files with indent for readability
- Read JSON back and process it as Python objects
- Handle custom types like datetime with a custom encoder
- Avoid common pitfalls (encoding, file modes, duplicate keys)
You now have a powerful skill for configuration files, API responses, and data persistence. In the next lesson, “Work with environment variables using the os module”, you'll learn how to store sensitive data like API keys outside your code using os.environ. Ready to keep your secrets safe?
Practice recap
Create a mini program that stores a dictionary of user preferences (theme, language, volume). Use json.dump() to save it to a preferences.json file. Then write a second function that reads the file with json.load() and prints each setting. Add error handling for when the file doesn't exist yet. Challenge: Add a custom serializer for a datetime object recording the last save time.
Common mistakes
- Using
json.loads()on a file object instead of a string, causing AttributeError — always matchdump(file) withload, anddumps(string) withloads. - Forgetting to open files in text mode with
encoding='utf-8'(Python 3 defaults to UTF-8 but explicit is safer) and writing multiple JSON objects without wrapping them in a list. - Assuming JSON preserves Python-specific types like
datetime,Decimal, orset— these must be converted to strings, floats, or lists manually before serialization. - Calling
json.dump()multiple times on the same file without overwriting, producing concatenated JSON (invalid) — always write your entire structure in one call.
Variations
- Use
json.dumps(fileobj, default=encoder)for custom object serialization (e.g.,datetimeto ISO string). - Substitute
json.loads()withjson.load(file)when working directly with file handles — fewer lines and fewer errors. - For extreme performance with large JSON arrays, consider
ijson(iterative JSON parser) instead ofjson.loads()which loads the entire file into memory.
Real-world use cases
- Saving user application settings as a JSON config file — read on startup, write on change.
- Parsing responses from REST APIs like GitHub, Twitter, or weather services that return JSON.
- Exporting SQL query results to a JSON file for import into other tools like Elasticsearch or web dashboards.
Key takeaways
- JSON is a universal, human-readable data format — Python's
jsonmodule converts between dicts/lists and JSON strings/files. - Use
json.dumps()→ string for memory/network;json.dump()→ file for persistence — always passindentfor readability. - Use
json.loads()from string;json.load()from file — handleJSONDecodeErrorwith try/except for malformed data. - Python types like
datetime,Decimal, andsetrequire custom serialization; usedefaultparameter orJSONEncodersubclass. - Always open JSON files with
open(filename, 'r', encoding='utf-8')to support international characters and avoid encoding errors.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.