At some point in nearly every Python project I’ve worked on — whether it’s a REST API, a config file, or a data pipeline feeding into a JavaScript frontend — I’ve had to take a Python dictionary and turn it into JSON. It sounds trivial, and for the simple cases it genuinely is, but I’ve hit enough edge cases over the years (datetime objects, custom classes, encoding quirks) that I think it’s worth walking through this properly, from the basics to the details that only show up once you’re working with real, messy data.
What Is JSON and Why Dicts Map So Naturally to It
JSON (JavaScript Object Notation) is a lightweight, text-based data interchange format. It’s become the de facto standard for APIs, configuration files, and data storage precisely because its structure — objects (key-value pairs), arrays, strings, numbers, booleans, and null — maps almost one-to-one onto Python’s own dict, list, str, int/float, bool, and None. That structural similarity is exactly why converting a Python dict to JSON feels so natural.
The json Module: Built Right Into Python
I don’t need to install anything — json has been part of the standard library since Python 2.6.
import json
Basic Conversion: dict to JSON String
The core function here is json.dumps() — “dump string.” It takes a Python object and returns a JSON-formatted string.
import json
person = {
"name": "Alice",
"age": 30,
"is_employee": True,
"skills": ["Python", "SQL", "Docker"],
"manager": None
}
json_string = json.dumps(person)
print(json_string)
print(type(json_string))
Output:
{"name": "Alice", "age": 30, "is_employee": true, "skills": ["Python", "SQL", "Docker"], "manager": null}
<class 'str'>
Notice the type conversions that happen automatically: Python’s True becomes JSON’s true, and None becomes null. This mapping is defined explicitly in the json module’s conversion table.
Writing JSON Directly to a File
When I need to persist the JSON rather than just print it, I use json.dump() (no “s” — this one writes directly to a file object rather than returning a string).
import json
data = {"project": "inventory_system", "version": "1.2.0", "active": True}
with open('config.json', 'w') as file:
json.dump(data, file)
I always use a with block here so the file handle closes automatically, even if an error occurs partway through writing.
Formatting JSON for Readability
Compact JSON is fine for machine-to-machine communication, but when I need to read the output myself — debugging, config files, logs — I format it using the indent parameter.
import json
data = {
"name": "Bob",
"address": {
"city": "Austin",
"zip": "78701"
},
"tags": ["engineer", "python"]
}
pretty_json = json.dumps(data, indent=4)
print(pretty_json)
Output:
{
"name": "Bob",
"address": {
"city": "Austin",
"zip": "78701"
},
"tags": [
"engineer",
"python"
]
}
I also frequently use sort_keys=True alongside indent when comparing JSON output across test runs, since it guarantees a consistent key order regardless of the dict’s internal ordering.
json.dumps(data, indent=4, sort_keys=True)
The Type Conversion Table
Understanding exactly what converts to what has saved me from a lot of confusion, especially around numeric edge cases:
| Python | JSON |
|---|---|
| dict | object |
| list, tuple | array |
| str | string |
| int, float | number |
| True | true |
| False | false |
| None | null |
Tuples are worth flagging specifically — they get converted to JSON arrays, just like lists, meaning the distinction between tuple and list is lost in the JSON output. If you need to preserve that information, you have to encode it yourself, since JSON has no native concept of a tuple.
Handling Data Types JSON Doesn’t Understand
This is where things get interesting, and where I’ve spent the most debugging time historically. JSON has no native representation for Python types like datetime, set, Decimal, or custom class instances. Trying to serialize them directly raises a TypeError.
import json
from datetime import datetime
data = {"created_at": datetime.now()}
try:
json.dumps(data)
except TypeError as e:
print(f"Error: {e}")
Output:
Error: Object of type datetime is not JSON serializable
The fix is to provide a custom default function that tells json.dumps() how to convert unsupported types:
import json
from datetime import datetime
def json_default(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
data = {"created_at": datetime.now(), "event": "login"}
print(json.dumps(data, default=json_default))
Output:
{"created_at": "2026-07-30T10:15:32.123456", "event": "login"}
I use this pattern constantly, especially in Django or Flask projects where model objects often contain datetime fields.
Internal Working: How dumps() Actually Serializes
Under the hood, json.dumps() walks the Python object recursively. For a dict, it iterates over key-value pairs; for each value, it checks the type against its internal encoder table and either serializes it directly (for natively supported types) or recurses further (for nested dicts/lists) or calls the default function (for anything unrecognized). This recursive-descent approach is why deeply nested structures work seamlessly — the encoder doesn’t need to know the shape of your data in advance, it just keeps recursing until it hits a base case.
CPython actually ships both a pure-Python implementation and a C-accelerated version (_json) of the encoder/decoder. By default, json.dumps() uses the C-accelerated version when available, which is why the module performs well even on fairly large structures — this matters if you’re serializing large API responses or big configuration objects repeatedly.
Performance Considerations
For most everyday use, json.dumps() performance is a non-issue. But when I’m serializing large volumes of data — like exporting thousands of records — a few things matter:
import json
import time
large_data = [{"id": i, "value": f"item_{i}"} for i in range(100000)]
start = time.perf_counter()
json_str = json.dumps(large_data)
print(f"Serialized in {time.perf_counter() - start:.4f} seconds")
Using indent adds noticeable overhead for large structures because it introduces extra formatting logic on every recursive call — I skip it entirely for machine-to-machine payloads and API responses, reserving it only for debug output and configuration files meant for humans to read.
Custom Objects: Serializing Class Instances
A pattern I use a lot when working with custom classes is converting the object to a dict first, then serializing that dict:
import json
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def to_dict(self):
return {"name": self.name, "price": self.price}
product = Product("Widget", 9.99)
print(json.dumps(product.to_dict()))
Alternatively, for automatic handling across many objects, I define a default function that checks for a to_dict() method:
def default_encoder(obj):
if hasattr(obj, 'to_dict'):
return obj.to_dict()
raise TypeError(f"Type {type(obj)} not serializable")
print(json.dumps({"product": product}, default=default_encoder))
Handling Non-ASCII Characters
By default, json.dumps() escapes all non-ASCII characters using \uXXXX sequences.
import json
data = {"city": "Zürich", "greeting": "こんにちは"}
print(json.dumps(data))
print(json.dumps(data, ensure_ascii=False))
Output:
{"city": "Z\u00fcrich", "greeting": "\u3053\u3093\u306b\u3061\u306f"}
{"city": "Zürich", "greeting": "こんにちは"}
I set ensure_ascii=False whenever I know the consuming system handles UTF-8 properly (most modern systems do), since it keeps the output human-readable and slightly more compact.
Common Mistakes I’ve Made
- Forgetting that dict keys must be strings in valid JSON. Python allows non-string dict keys, and
json.dumps()will silently convert them to strings, which can cause subtle bugs if you’re not expecting it.
data = {1: "one", 2: "two"}
print(json.dumps(data)) # {"1": "one", "2": "two"} - keys became strings
- Trying to serialize a set without converting it to a list first — sets aren’t JSON serializable natively.
- Assuming tuples stay tuples after a round trip through JSON — they become lists, and that information is lost permanently unless handled explicitly.
- Not handling
TypeErrorwhen serializing objects from external libraries with unpredictable attribute types.
Real-World Use Cases
- Building REST API responses in frameworks like Flask or FastAPI.
- Writing configuration files that are both human-readable and easy to parse programmatically.
- Logging structured data for downstream log-processing tools that expect JSON lines.
- Serializing data for message queues like RabbitMQ or Kafka, where JSON is a common wire format.
FAQs
What’s the difference between json.dumps() and json.dump()? dumps() (with an “s”) returns a JSON string. dump() writes JSON directly to a file-like object and returns None.
Can I convert a JSON string back into a Python dict? Yes, using json.loads() for a string, or json.load() for a file object — these are the inverse operations of dumps()/dump().
Why does my float look different after JSON conversion? JSON numbers are represented as text, and floating-point representation can introduce tiny precision differences. For financial data, I use Decimal with a custom encoder rather than raw floats.
How do I keep dictionary key order in the output? Since Python 3.7, dicts preserve insertion order by default, and json.dumps() respects that order unless you pass sort_keys=True.
Is JSON serialization safe for untrusted data? Serializing (dict to JSON) is safe. Deserializing (JSON to Python) with json.loads() is also safe by default, unlike pickle, since JSON doesn’t support arbitrary code execution — this is one reason JSON is preferred over pickle for untrusted or cross-language data.
Summary
Converting a Python dict to JSON is one of those tasks that looks like a one-liner until real-world data — datetimes, custom objects, non-ASCII text, non-string keys — forces you to understand what’s actually happening underneath json.dumps(). Once I internalized the type conversion table and got comfortable writing custom default encoders, JSON serialization stopped being something I debugged reactively and became something I could design correctly from the start.
References
- Python Official Documentation: json — JSON encoder and decoder
- Python Official Documentation: json.dumps() and encoding details
- RFC 8259 – The JavaScript Object Notation (JSON) Data Interchange Format: https://www.rfc-editor.org/rfc/rfc8259